mirror of
https://github.com/alerta/alerta.git
synced 2025-01-30 11:36:20 +00:00
453 lines
15 KiB
Python
453 lines
15 KiB
Python
"""Utilities and helper functions."""
|
|
|
|
import json
|
|
import datetime
|
|
|
|
|
|
def relative_date(from_date, now=None):
|
|
|
|
now = now or datetime.datetime.utcnow()
|
|
diff = now - from_date
|
|
|
|
if now < from_date:
|
|
return 'in the future'
|
|
else:
|
|
when = 'ago'
|
|
|
|
secs = diff.seconds
|
|
days = diff.days
|
|
|
|
if days == 0:
|
|
if secs == 0:
|
|
return 'just now'
|
|
if secs < 90:
|
|
return '{} seconds {}'.format(secs, when)
|
|
mins = (secs + 30) / 60
|
|
if mins < 90:
|
|
return '{} minutes {}'.format(mins, when)
|
|
else:
|
|
hrs = (mins + 30) / 60
|
|
return '{} hours {}'.format(hrs, when)
|
|
|
|
if days == 1:
|
|
hrs = 24 + ((secs + 1800) / 3600)
|
|
return '{} hours {}'.format(hrs, when)
|
|
|
|
days += (((((secs + 30) / 60) + 30) / 60) + 12) / 24
|
|
if days < 14:
|
|
return '{} days {}'.format(days, when)
|
|
elif days < 70:
|
|
return '{} weeks {}'.format((days + 3) / 7, when)
|
|
elif days < 365:
|
|
return '{} months {}'.format((days + 15) / 30, when)
|
|
elif days < 1825:
|
|
total_months = (days * 12 * 2 + 365) / (365 * 2)
|
|
years = total_months / 12
|
|
months = total_months % 12
|
|
return '{} year{}, {} month{} {}'.format(years, ('s' if years > 1 else ''),
|
|
months, ('s' if months > 1 else ''), when)
|
|
else:
|
|
return '{} years {}'.format((days + 183) / 365, when)
|
|
|
|
|
|
# Extend JSON Encoder to support ISO 8601 format dates
|
|
class DateEncoder(json.JSONEncoder):
|
|
def default(self, obj):
|
|
|
|
if isinstance(obj, (datetime.date, datetime.datetime)):
|
|
return obj.replace(microsecond=0).strftime('%Y-%m-%dT%H:%M:%S') + ".%03dZ" % (obj.microsecond // 1000)
|
|
else:
|
|
return json.JSONEncoder.default(self, obj)
|
|
|
|
|
|
class Bunch(dict):
|
|
"""
|
|
Copyright (c) 2010 David Schoonover
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
A dictionary that provides attribute-style access.
|
|
|
|
>>> b = Bunch()
|
|
>>> b.hello = 'world'
|
|
>>> b.hello
|
|
'world'
|
|
>>> b['hello'] += "!"
|
|
>>> b.hello
|
|
'world!'
|
|
>>> b.foo = Bunch(lol=True)
|
|
>>> b.foo.lol
|
|
True
|
|
>>> b.foo is b['foo']
|
|
True
|
|
|
|
A Bunch is a subclass of dict; it supports all the methods a dict does...
|
|
|
|
>>> b.keys()
|
|
['foo', 'hello']
|
|
|
|
Including update()...
|
|
|
|
>>> b.update({ 'ponies': 'are pretty!' }, hello=42)
|
|
>>> print repr(b)
|
|
Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
|
|
As well as iteration...
|
|
|
|
>>> [ (k,b[k]) for k in b ]
|
|
[('ponies', 'are pretty!'), ('foo', Bunch(lol=True)), ('hello', 42)]
|
|
|
|
And "splats".
|
|
|
|
>>> "The {knights} who say {ni}!".format(**Bunch(knights='lolcats', ni='can haz'))
|
|
'The lolcats who say can haz!'
|
|
|
|
See unbunchify/Bunch.toDict, bunchify/Bunch.fromDict for notes about conversion.
|
|
"""
|
|
|
|
def __contains__(self, k):
|
|
""" >>> b = Bunch(ponies='are pretty!')
|
|
>>> 'ponies' in b
|
|
True
|
|
>>> 'foo' in b
|
|
False
|
|
>>> b['foo'] = 42
|
|
>>> 'foo' in b
|
|
True
|
|
>>> b.hello = 'hai'
|
|
>>> 'hello' in b
|
|
True
|
|
"""
|
|
try:
|
|
return hasattr(self, k) or dict.__contains__(self, k)
|
|
except:
|
|
return False
|
|
|
|
# only called if k not found in normal places
|
|
def __getattr__(self, k):
|
|
""" Gets key if it exists, otherwise throws AttributeError.
|
|
|
|
nb. __getattr__ is only called if key is not found in normal places.
|
|
|
|
>>> b = Bunch(bar='baz', lol={})
|
|
>>> b.foo
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: foo
|
|
|
|
>>> b.bar
|
|
'baz'
|
|
>>> getattr(b, 'bar')
|
|
'baz'
|
|
>>> b['bar']
|
|
'baz'
|
|
|
|
>>> b.lol is b['lol']
|
|
True
|
|
>>> b.lol is getattr(b, 'lol')
|
|
True
|
|
"""
|
|
try:
|
|
# Throws exception if not in prototype chain
|
|
return object.__getattribute__(self, k)
|
|
except AttributeError:
|
|
try:
|
|
return self[k]
|
|
except KeyError:
|
|
return None
|
|
|
|
def __setattr__(self, k, v):
|
|
""" Sets attribute k if it exists, otherwise sets key k. A KeyError
|
|
raised by set-item (only likely if you subclass Bunch) will
|
|
propagate as an AttributeError instead.
|
|
|
|
>>> b = Bunch(foo='bar', this_is='useful when subclassing')
|
|
>>> b.values #doctest: +ELLIPSIS
|
|
<built-in method values of Bunch object at 0x...>
|
|
>>> b.values = 'uh oh'
|
|
>>> b.values
|
|
'uh oh'
|
|
>>> b['values']
|
|
Traceback (most recent call last):
|
|
...
|
|
KeyError: 'values'
|
|
"""
|
|
try:
|
|
# Throws exception if not in prototype chain
|
|
object.__getattribute__(self, k)
|
|
except AttributeError:
|
|
try:
|
|
self[k] = v
|
|
except:
|
|
raise AttributeError(k)
|
|
else:
|
|
object.__setattr__(self, k, v)
|
|
|
|
def __delattr__(self, k):
|
|
""" Deletes attribute k if it exists, otherwise deletes key k. A KeyError
|
|
raised by deleting the key--such as when the key is missing--will
|
|
propagate as an AttributeError instead.
|
|
|
|
>>> b = Bunch(lol=42)
|
|
>>> del b.values
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: 'Bunch' object attribute 'values' is read-only
|
|
>>> del b.lol
|
|
>>> b.lol
|
|
Traceback (most recent call last):
|
|
...
|
|
AttributeError: lol
|
|
"""
|
|
try:
|
|
# Throws exception if not in prototype chain
|
|
object.__getattribute__(self, k)
|
|
except AttributeError:
|
|
try:
|
|
del self[k]
|
|
except KeyError:
|
|
raise AttributeError(k)
|
|
else:
|
|
object.__delattr__(self, k)
|
|
|
|
def toDict(self):
|
|
""" Recursively converts a bunch back into a dictionary.
|
|
|
|
>>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
>>> b.toDict()
|
|
{'ponies': 'are pretty!', 'foo': {'lol': True}, 'hello': 42}
|
|
|
|
See unbunchify for more info.
|
|
"""
|
|
return unbunchify(self)
|
|
|
|
def __repr__(self):
|
|
""" Invertible* string-form of a Bunch.
|
|
|
|
>>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
>>> print repr(b)
|
|
Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
>>> eval(repr(b))
|
|
Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
|
|
(*) Invertible so long as collection contents are each repr-invertible.
|
|
"""
|
|
keys = self.keys()
|
|
keys.sort()
|
|
args = ', '.join(['%s=%r' % (key, self[key]) for key in keys])
|
|
return '%s(%s)' % (self.__class__.__name__, args)
|
|
|
|
@staticmethod
|
|
def fromDict(d):
|
|
""" Recursively transforms a dictionary into a Bunch via copy.
|
|
|
|
>>> b = Bunch.fromDict({'urmom': {'sez': {'what': 'what'}}})
|
|
>>> b.urmom.sez.what
|
|
'what'
|
|
|
|
See bunchify for more info.
|
|
"""
|
|
return bunchify(d)
|
|
|
|
|
|
|
|
# While we could convert abstract types like Mapping or Iterable, I think
|
|
# bunchify is more likely to "do what you mean" if it is conservative about
|
|
# casting (ex: isinstance(str,Iterable) == True ).
|
|
#
|
|
# Should you disagree, it is not difficult to duplicate this function with
|
|
# more aggressive coercion to suit your own purposes.
|
|
|
|
def bunchify(x):
|
|
""" Recursively transforms a dictionary into a Bunch via copy.
|
|
|
|
>>> b = bunchify({'urmom': {'sez': {'what': 'what'}}})
|
|
>>> b.urmom.sez.what
|
|
'what'
|
|
|
|
bunchify can handle intermediary dicts, lists and tuples (as well as
|
|
their subclasses), but ymmv on custom datatypes.
|
|
|
|
>>> b = bunchify({ 'lol': ('cats', {'hah':'i win again'}),
|
|
... 'hello': [{'french':'salut', 'german':'hallo'}] })
|
|
>>> b.hello[0].french
|
|
'salut'
|
|
>>> b.lol[1].hah
|
|
'i win again'
|
|
|
|
nb. As dicts are not hashable, they cannot be nested in sets/frozensets.
|
|
"""
|
|
if isinstance(x, dict):
|
|
return Bunch( (k, bunchify(v)) for k,v in x.iteritems() )
|
|
elif isinstance(x, (list, tuple)):
|
|
return type(x)( bunchify(v) for v in x )
|
|
else:
|
|
return x
|
|
|
|
def unbunchify(x):
|
|
""" Recursively converts a Bunch into a dictionary.
|
|
|
|
>>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
>>> unbunchify(b)
|
|
{'ponies': 'are pretty!', 'foo': {'lol': True}, 'hello': 42}
|
|
|
|
unbunchify will handle intermediary dicts, lists and tuples (as well as
|
|
their subclasses), but ymmv on custom datatypes.
|
|
|
|
>>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42,
|
|
... ponies=('are pretty!', Bunch(lies='are trouble!')))
|
|
>>> unbunchify(b) #doctest: +NORMALIZE_WHITESPACE
|
|
{'ponies': ('are pretty!', {'lies': 'are trouble!'}),
|
|
'foo': ['bar', {'lol': True}], 'hello': 42}
|
|
|
|
nb. As dicts are not hashable, they cannot be nested in sets/frozensets.
|
|
"""
|
|
if isinstance(x, dict):
|
|
return dict( (k, unbunchify(v)) for k,v in x.iteritems() )
|
|
elif isinstance(x, (list, tuple)):
|
|
return type(x)( unbunchify(v) for v in x )
|
|
else:
|
|
return x
|
|
|
|
|
|
### Serialization
|
|
|
|
try:
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
|
|
def toJSON(self, **options):
|
|
""" Serializes this Bunch to JSON. Accepts the same keyword options as `json.dumps()`.
|
|
|
|
>>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!')
|
|
>>> json.dumps(b)
|
|
'{"ponies": "are pretty!", "foo": {"lol": true}, "hello": 42}'
|
|
>>> b.toJSON()
|
|
'{"ponies": "are pretty!", "foo": {"lol": true}, "hello": 42}'
|
|
"""
|
|
return json.dumps(self, **options)
|
|
|
|
Bunch.toJSON = toJSON
|
|
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
# Attempt to register ourself with PyYAML as a representer
|
|
import yaml
|
|
from yaml.representer import Representer, SafeRepresenter
|
|
|
|
def from_yaml(loader, node):
|
|
""" PyYAML support for Bunches using the tag `!bunch` and `!bunch.Bunch`.
|
|
|
|
>>> import yaml
|
|
>>> yaml.load('''
|
|
... Flow style: !bunch.Bunch { Clark: Evans, Brian: Ingerson, Oren: Ben-Kiki }
|
|
... Block style: !bunch
|
|
... Clark : Evans
|
|
... Brian : Ingerson
|
|
... Oren : Ben-Kiki
|
|
... ''') #doctest: +NORMALIZE_WHITESPACE
|
|
{'Flow style': Bunch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki'),
|
|
'Block style': Bunch(Brian='Ingerson', Clark='Evans', Oren='Ben-Kiki')}
|
|
|
|
This module registers itself automatically to cover both Bunch and any
|
|
subclasses. Should you want to customize the representation of a subclass,
|
|
simply register it with PyYAML yourself.
|
|
"""
|
|
data = Bunch()
|
|
yield data
|
|
value = loader.construct_mapping(node)
|
|
data.update(value)
|
|
|
|
|
|
def to_yaml_safe(dumper, data):
|
|
""" Converts Bunch to a normal mapping node, making it appear as a
|
|
dict in the YAML output.
|
|
|
|
>>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42)
|
|
>>> import yaml
|
|
>>> yaml.safe_dump(b, default_flow_style=True)
|
|
'{foo: [bar, {lol: true}], hello: 42}\\n'
|
|
"""
|
|
return dumper.represent_dict(data)
|
|
|
|
def to_yaml(dumper, data):
|
|
""" Converts Bunch to a representation node.
|
|
|
|
>>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42)
|
|
>>> import yaml
|
|
>>> yaml.dump(b, default_flow_style=True)
|
|
'!bunch.Bunch {foo: [bar, !bunch.Bunch {lol: true}], hello: 42}\\n'
|
|
"""
|
|
return dumper.represent_mapping(u'!bunch.Bunch', data)
|
|
|
|
|
|
yaml.add_constructor(u'!bunch', from_yaml)
|
|
yaml.add_constructor(u'!bunch.Bunch', from_yaml)
|
|
|
|
SafeRepresenter.add_representer(Bunch, to_yaml_safe)
|
|
SafeRepresenter.add_multi_representer(Bunch, to_yaml_safe)
|
|
|
|
Representer.add_representer(Bunch, to_yaml)
|
|
Representer.add_multi_representer(Bunch, to_yaml)
|
|
|
|
|
|
# Instance methods for YAML conversion
|
|
def toYAML(self, **options):
|
|
""" Serializes this Bunch to YAML, using `yaml.safe_dump()` if
|
|
no `Dumper` is provided. See the PyYAML documentation for more info.
|
|
|
|
>>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42)
|
|
>>> import yaml
|
|
>>> yaml.safe_dump(b, default_flow_style=True)
|
|
'{foo: [bar, {lol: true}], hello: 42}\\n'
|
|
>>> b.toYAML(default_flow_style=True)
|
|
'{foo: [bar, {lol: true}], hello: 42}\\n'
|
|
>>> yaml.dump(b, default_flow_style=True)
|
|
'!bunch.Bunch {foo: [bar, !bunch.Bunch {lol: true}], hello: 42}\\n'
|
|
>>> b.toYAML(Dumper=yaml.Dumper, default_flow_style=True)
|
|
'!bunch.Bunch {foo: [bar, !bunch.Bunch {lol: true}], hello: 42}\\n'
|
|
"""
|
|
opts = dict(indent=4, default_flow_style=False)
|
|
opts.update(options)
|
|
if 'Dumper' not in opts:
|
|
return yaml.safe_dump(self, **opts)
|
|
else:
|
|
return yaml.dump(self, **opts)
|
|
|
|
def fromYAML(*args, **kwargs):
|
|
return bunchify( yaml.load(*args, **kwargs) )
|
|
|
|
Bunch.toYAML = Bunch.__str__ = toYAML
|
|
Bunch.fromYAML = staticmethod(fromYAML)
|
|
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
doctest.testmod()
|
|
|