Page MenuHomePhabricator

No OneTemporary

diff --git a/sipsimple/addressbook.py b/sipsimple/addressbook.py
index 2cc7b464..aa70b9b1 100644
--- a/sipsimple/addressbook.py
+++ b/sipsimple/addressbook.py
@@ -1,1454 +1,1455 @@
# Copyright (C) 2011 AG Projects. See LICENSE for details.
#
"""Implementation of an addressbook management system"""
from __future__ import absolute_import
__all__ = ['AddressbookManager', 'Contact', 'ContactURI', 'Group', 'Policy', 'SharedSetting', 'ContactExtension', 'ContactURIExtension', 'GroupExtension', 'PolicyExtension']
from functools import reduce
from operator import attrgetter
from random import randint
from threading import Lock
from time import time
from zope.interface import implements
from application import log
from application.notification import IObserver, NotificationCenter
from application.python import Null
from application.python.decorator import execute_once
-from application.python.types import Singleton
+from application.python.types import Singleton, MarkerType
+from application.python.weakref import weakobjectmap
from sipsimple.account import xcap, AccountManager
from sipsimple.configuration import ConfigurationManager, AbstractSetting, SettingsGroup, SettingsGroupMeta, SettingsObjectImmutableID, SettingsState, PersistentKey, ItemContainer, ModifiedValue
from sipsimple.configuration import ObjectNotFoundError, DuplicateIDError
from sipsimple.payloads.addressbook import PolicyValue, ElementAttributes
from sipsimple.payloads.datatypes import ID
from sipsimple.payloads.resourcelists import ResourceListsDocument
from sipsimple.threading import run_in_thread
-from sipsimple.util import MarkerType, TimestampedNotificationData, weakobjectmap
+from sipsimple.util import TimestampedNotificationData
def unique_id(prefix='id'):
return "%s%d%06d" % (prefix, time()*1e6, randint(0, 999999))
def recursive_getattr(obj, name):
return reduce(getattr, name.split('.'), obj)
class Local(object):
__metaclass__ = MarkerType
class Remote(object):
def __init__(self, account, xcap_object):
self.account = account
self.xcap_object = xcap_object
def __repr__(self):
return "%s(%r, %r)" % (self.__class__.__name__, self.account, self.xcap_object)
class defaultweakobjectmap(weakobjectmap):
def __init__(self, factory, *args, **kw):
self.default_factory = factory
super(defaultweakobjectmap, self).__init__(*args, **kw)
def __missing__(self, key):
return self.setdefault(key.object, self.default_factory())
class ModifiedList(object):
__slots__ = ('added', 'removed', 'modified')
def __init__(self, added, removed, modified):
self.added = added
self.removed = removed
self.modified = modified
def __repr__(self):
return '%s(added=%r, removed=%r, modified=%r)' % (self.__class__.__name__, self.added, self.removed, self.modified)
class Setting(AbstractSetting):
"""
Descriptor represeting a setting in an addressbook object.
Unlike a standard Setting, this one will only use the default value as a
template to fill in a missing value and explicitly set it when saving if
it was not specified explicitly prior to that.
"""
def __init__(self, type, default=None, nillable=False):
if default is None and not nillable:
raise TypeError("default must be specified if object is not nillable")
self.type = type
self.default = default
self.nillable = nillable
self.values = defaultweakobjectmap(lambda: default)
self.oldvalues = defaultweakobjectmap(lambda: default)
self.dirty = defaultweakobjectmap(lambda: False)
self.lock = Lock()
def __get__(self, obj, objtype):
if obj is None:
return self
with self.lock:
return self.values[obj]
def __set__(self, obj, value):
if value is None and not self.nillable:
raise ValueError("setting attribute is not nillable")
if value is not None and not isinstance(value, self.type):
value = self.type(value)
with self.lock:
self.values[obj] = value
self.dirty[obj] = value != self.oldvalues[obj]
def __getstate__(self, obj):
with self.lock:
value = self.values[obj]
if value is None:
pass
elif issubclass(self.type, bool):
value = u'true' if value else u'false'
elif issubclass(self.type, (int, long, basestring)):
value = unicode(value)
else:
try:
value = value.__getstate__()
except AttributeError:
raise TypeError("Setting type %s does not provide __getstate__" % value.__class__.__name__)
return value
def __setstate__(self, obj, value):
if value is None and not self.nillable:
raise ValueError("setting attribute is not nillable")
if value is None:
pass
elif issubclass(self.type, bool):
if value.lower() in ('true', 'yes', 'on', '1'):
value = True
elif value.lower() in ('false', 'no', 'off', '0'):
value = False
else:
raise ValueError("invalid boolean value: %s" % (value,))
elif issubclass(self.type, (int, long, basestring)):
value = self.type(value)
else:
object = self.type.__new__(self.type)
object.__setstate__(value)
value = object
with self.lock:
self.oldvalues[obj] = self.values[obj] = value
self.dirty[obj] = False
def get_modified(self, obj):
with self.lock:
try:
if self.dirty[obj]:
return ModifiedValue(old=self.oldvalues[obj], new=self.values[obj])
else:
return None
finally:
self.oldvalues[obj] = self.values[obj]
self.dirty[obj] = False
def get_old(self, obj):
with self.lock:
return self.oldvalues[obj]
def undo(self, obj):
with self.lock:
self.values[obj] = self.oldvalues[obj]
self.dirty[obj] = False
class SharedSetting(Setting):
"""A setting that is shared by being also stored remotely in XCAP"""
__namespace__ = None
@classmethod
def set_namespace(cls, namespace):
"""
Set the XML namespace to be used for the extra shared attributes of
a contact, when storing it in XCAP
"""
if cls.__namespace__ is not None:
raise RuntimeError("namespace already set to %s" % cls.__namespace__)
cls.__namespace__ = namespace
class ApplicationElementAttributes(ElementAttributes):
_xml_namespace = 'urn:%s:xml:ns:addressbook' % namespace
ResourceListsDocument.unregister_namespace(ElementAttributes._xml_namespace)
ResourceListsDocument.register_namespace(ApplicationElementAttributes._xml_namespace, prefix=namespace.rpartition(':')[2])
for cls, attribute_name in ((cls, name) for cls in ResourceListsDocument.element_map.values() for name, elem in cls._xml_element_children.items() if elem.type is ElementAttributes):
cls.unregister_extension(attribute_name)
cls.register_extension(attribute_name, ApplicationElementAttributes)
class AddressbookKey(object):
def __init__(self, section):
self.group = 'Addressbook'
self.section = section
def __get__(self, obj, objtype):
if obj is None:
return [self.group, self.section]
else:
return [self.group, self.section, PersistentKey(obj.__id__)]
def __set__(self, obj, value):
raise AttributeError('cannot set attribute')
def __delete__(self, obj):
raise AttributeError('cannot delete attribute')
class MultiAccountTransaction(object):
def __init__(self, accounts):
self.accounts = accounts
def __enter__(self):
for account in self.accounts:
account.xcap_manager.start_transaction()
return self
def __exit__(self, exc_type, exc_value, traceback):
for account in self.accounts:
account.xcap_manager.commit_transaction()
def __iter__(self):
return iter(self.accounts)
class XCAPGroup(xcap.Group):
"""An XCAP Group with attributes normalized to unicode"""
__attributes__ = set()
def __init__(self, id, name, contacts, **attributes):
normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__)
contacts = [XCAPContact.normalize(contact) for contact in contacts]
super(XCAPGroup, self).__init__(id, name, contacts, **normalized_attributes)
@classmethod
def normalize(cls, group):
return cls(group.id, group.name, group.contacts, **group.attributes)
def get_modified(self, modified_keys):
names = set(['name'])
attributes = dict((name, getattr(self, name)) for name in names.intersection(modified_keys))
attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys))
return attributes
class XCAPContactURI(xcap.ContactURI):
"""An XCAP ContactURI with attributes normalized to unicode"""
__attributes__ = set()
def __init__(self, id, uri, type, **attributes):
normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__)
super(XCAPContactURI, self).__init__(id, uri, type, **normalized_attributes)
@classmethod
def normalize(cls, uri):
return cls(uri.id, uri.uri, uri.type, **uri.attributes)
def get_modified(self, modified_keys):
names = set(['uri', 'type'])
attributes = dict((name, getattr(self, name)) for name in names.intersection(modified_keys))
attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys))
return attributes
class XCAPContact(xcap.Contact):
"""An XCAP Contact with attributes normalized to unicode"""
__attributes__ = set()
def __init__(self, id, name, uris, presence_handling=None, dialog_handling=None, **attributes):
normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__)
uris = [XCAPContactURI.normalize(uri) for uri in uris]
super(XCAPContact, self).__init__(id, name, uris, presence_handling, dialog_handling, **normalized_attributes)
@classmethod
def normalize(cls, contact):
return cls(contact.id, contact.name, contact.uris, contact.presence, contact.dialog, **contact.attributes)
def get_modified(self, modified_keys):
names = set(['name', 'presence.policy', 'presence.subscribe', 'dialog.policy', 'dialog.subscribe'])
attributes = dict((name, recursive_getattr(self, name)) for name in names.intersection(modified_keys))
attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys))
return attributes
class XCAPPolicy(xcap.Policy):
"""An XCAP Policy with attributes normalized to unicode"""
__attributes__ = set()
def __init__(self, id, uri, name, presence_handling=None, dialog_handling=None, **attributes):
normalized_attributes = dict((name, unicode(value) if value is not None else None) for name, value in attributes.iteritems() if name in self.__attributes__)
super(XCAPPolicy, self).__init__(id, uri, name, presence_handling, dialog_handling, **normalized_attributes)
@classmethod
def normalize(cls, policy):
return cls(policy.id, policy.uri, policy.name, policy.presence, policy.dialog, **policy.attributes)
def get_modified(self, modified_keys):
names = set(['uri', 'name', 'presence.policy', 'presence.subscribe', 'dialog.policy', 'dialog.subscribe'])
attributes = dict((name, recursive_getattr(self, name)) for name in names.intersection(modified_keys))
attributes.update((name, self.attributes[name]) for name in self.__attributes__.intersection(modified_keys))
return attributes
class ContactListDescriptor(AbstractSetting):
def __init__(self):
self.values = defaultweakobjectmap(ContactList)
self.oldvalues = defaultweakobjectmap(ContactList)
self.lock = Lock()
def __get__(self, obj, objtype):
if obj is None:
return self
with self.lock:
return self.values[obj]
def __set__(self, obj, value):
if value is None:
raise ValueError("setting attribute is not nillable")
elif not isinstance(value, ContactList):
value = ContactList(value)
with self.lock:
self.values[obj] = value
def __getstate__(self, obj):
with self.lock:
return self.values[obj].__getstate__()
def __setstate__(self, obj, value):
if value is None:
raise ValueError("setting attribute is not nillable")
object = ContactList.__new__(ContactList)
object.__setstate__(value)
with self.lock:
self.values[obj] = object
self.oldvalues[obj] = ContactList(object)
def get_modified(self, obj):
with self.lock:
old = self.oldvalues[obj]
new = self.values[obj]
with new.lock:
old_ids = set(old.ids())
new_ids = set(new.ids())
added_contacts = [new[id] for id in new_ids - old_ids]
removed_contacts = [old[id] for id in old_ids - new_ids]
try:
if added_contacts or removed_contacts:
return ModifiedList(added=added_contacts, removed=removed_contacts, modified=None)
else:
return None
finally:
self.oldvalues[obj] = ContactList(new)
def get_old(self, obj):
with self.lock:
return self.oldvalues[obj]
def undo(self, obj):
with self.lock:
self.values[obj] = ContactList(self.oldvalues[obj])
class ContactList(object):
def __new__(cls, contacts=None):
instance = object.__new__(cls)
instance.lock = Lock()
return instance
def __init__(self, contacts=None):
self.contacts = dict((contact.id, contact) for contact in contacts or [] if contact.__state__ != 'deleted')
def __getitem__(self, key):
return self.contacts[key]
def __contains__(self, key):
return key in self.contacts
def __iter__(self):
return iter(sorted(self.contacts.values(), key=attrgetter('id')))
def __reversed__(self):
return iter(sorted(self.contacts.values(), key=attrgetter('id'), reverse=True))
__hash__ = None
def __len__(self):
return len(self.contacts)
def __eq__(self, other):
if isinstance(other, ContactList):
return self.contacts == other.contacts
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, sorted(self.contacts.values(), key=attrgetter('id')))
def __getstate__(self):
return self.contacts.keys()
def __setstate__(self, value):
addressbook_manager = AddressbookManager()
for id in [id for id in value if not addressbook_manager.has_contact(id)]:
value.remove(id)
with self.lock:
self.contacts = dict((id, addressbook_manager.get_contact(id)) for id in value)
def ids(self):
return sorted(self.contacts.keys())
def add(self, contact):
if contact.__state__ == 'deleted':
return
with self.lock:
self.contacts[contact.id] = contact
def remove(self, contact):
with self.lock:
self.contacts.pop(contact.id, None)
class Group(SettingsState):
__key__ = AddressbookKey('Groups')
__id__ = SettingsObjectImmutableID(type=ID)
id = __id__
name = Setting(type=unicode, default='')
contacts = ContactListDescriptor()
def __new__(cls, id=None):
with AddressbookManager.load.lock:
if not AddressbookManager.load.called:
raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__)
if id is None:
id = unique_id()
elif not isinstance(id, basestring):
raise TypeError("id needs to be a string or unicode object")
instance = SettingsState.__new__(cls)
instance.__id__ = id
instance.__state__ = 'new'
instance.__xcapgroup__ = None
configuration = ConfigurationManager()
try:
data = configuration.get(instance.__key__)
except ObjectNotFoundError:
pass
else:
instance.__setstate__(data)
instance.__state__ = 'loaded'
instance.__xcapgroup__ = instance.__toxcap__()
return instance
def __establish__(self):
if self.__state__ == 'loaded':
self.__state__ = 'active'
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookGroupWasActivated', sender=self, data=TimestampedNotificationData())
def __repr__(self):
return "%s(id=%r)" % (self.__class__.__name__, self.id)
def __toxcap__(self):
xcap_contacts = [contact.__xcapcontact__ for contact in self.contacts]
attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting))
return XCAPGroup(self.id, self.name, xcap_contacts, **attributes)
@run_in_thread('file-io')
def _internal_save(self, originator):
if self.__state__ == 'deleted':
return
for contact in [contact for contact in self.contacts if contact.__state__ == 'deleted']:
self.contacts.remove(contact)
modified_settings = self.get_modified()
if not modified_settings and self.__state__ != 'new':
return
account_manager = AccountManager()
configuration = ConfigurationManager()
notification_center = NotificationCenter()
if originator is Local:
originator_account = None
previous_xcapgroup = self.__xcapgroup__
else:
originator_account = originator.account
previous_xcapgroup = originator.xcap_object
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
self.__xcapgroup__ = self.__toxcap__()
if self.__state__ == 'new':
configuration.update(self.__key__, self.__getstate__())
self.__state__ = 'active'
for account in (account for account in xcap_accounts if account is not originator_account):
account.xcap_manager.add_group(self.__xcapgroup__)
modified_data = None
notification_center.post_notification('AddressbookGroupWasActivated', sender=self, data=TimestampedNotificationData())
notification_center.post_notification('AddressbookGroupWasCreated', sender=self, data=TimestampedNotificationData())
else:
configuration.update(self.__key__, self.__getstate__())
attributes = self.__xcapgroup__.get_modified(modified_settings)
if 'contacts' in modified_settings:
added_contacts = [contact.__xcapcontact__ for contact in modified_settings['contacts'].added]
removed_contacts = [contact.__xcapcontact__ for contact in modified_settings['contacts'].removed]
else:
added_contacts = []
removed_contacts = []
if self.__xcapgroup__ != previous_xcapgroup:
outofsync_accounts = xcap_accounts
elif originator is Local:
outofsync_accounts = []
else:
outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account)
with MultiAccountTransaction(outofsync_accounts):
for account in outofsync_accounts:
xcap_manager = account.xcap_manager
for xcapcontact in added_contacts:
xcap_manager.add_group_member(self.__xcapgroup__, xcapcontact)
for xcapcontact in removed_contacts:
xcap_manager.remove_group_member(self.__xcapgroup__, xcapcontact)
if attributes:
xcap_manager.update_group(self.__xcapgroup__, attributes)
notification_center.post_notification('AddressbookGroupDidChange', sender=self, data=TimestampedNotificationData(modified=modified_settings))
modified_data = modified_settings
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='save', modified=modified_data, exception=e))
@run_in_thread('file-io')
def _internal_delete(self, originator):
if self.__state__ == 'deleted':
return
self.__state__ = 'deleted'
configuration = ConfigurationManager()
account_manager = AccountManager()
notification_center = NotificationCenter()
if originator is Local:
originator_account = None
else:
originator_account = originator.account
configuration.delete(self.__key__)
for account in (account for account in account_manager.iter_accounts() if hasattr(account, 'xcap') and account.xcap.discovered and account is not originator_account):
account.xcap_manager.remove_group(self.__xcapgroup__)
notification_center.post_notification('AddressbookGroupWasDeleted', sender=self, data=TimestampedNotificationData())
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='delete', exception=e))
def save(self):
"""
Store the group into persistent storage (local and xcap).
This method will post the AddressbookGroupWasCreated and
AddressbookGroupWasActivated notifications on the first save or a
AddressbookGroupDidChange notification on subsequent saves, regardless
of whether the contact has been saved to persistent storage or not.
A CFGManagerSaveFailed notification is posted if saving to the
persistent configuration storage fails.
"""
self._internal_save(originator=Local)
def delete(self):
"""Remove the group from the persistent storage."""
self._internal_delete(originator=Local)
def clone(self, new_id=None):
"""Create a copy of this group and all its sub-settings."""
raise NotImplementedError
@classmethod
def register_extension(cls, extension):
"""
Register an extension for this class. All Settings and SettingsGroups
defined in the extension will be added to this class, overwriting any
attributes with the same name. Other attributes in the extension are
ignored.
"""
if not issubclass(extension, GroupExtension):
raise TypeError("expected subclass of GroupExtension, got %r" % (extension,))
for name in dir(extension):
attribute = getattr(extension, name, None)
if isinstance(attribute, SharedSetting):
if SharedSetting.__namespace__ is None:
raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace")
XCAPGroup.__attributes__.add(name)
if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)):
setattr(cls, name, attribute)
class GroupExtension(object):
"""Base class for extensions of Groups"""
def __new__(cls, *args, **kw):
raise TypeError("GroupExtension subclasses cannot be instantiated")
class ContactURIListDescriptor(AbstractSetting):
def __init__(self):
self.values = defaultweakobjectmap(ContactURIList)
self.oldvalues = defaultweakobjectmap(ContactURIList)
self.lock = Lock()
def __get__(self, obj, objtype):
if obj is None:
return self
with self.lock:
return self.values[obj]
def __set__(self, obj, value):
if value is None:
raise ValueError("setting attribute is not nillable")
elif not isinstance(value, ContactURIList):
value = ContactURIList(value)
with self.lock:
self.values[obj] = value
def __getstate__(self, obj):
with self.lock:
return self.values[obj].__getstate__()
def __setstate__(self, obj, value):
if value is None:
raise ValueError("setting attribute is not nillable")
object = ContactURIList.__new__(ContactURIList)
object.__setstate__(value)
with self.lock:
self.values[obj] = object
self.oldvalues[obj] = ContactURIList(object)
def get_modified(self, obj):
with self.lock:
old = self.oldvalues[obj]
new = self.values[obj]
with new.lock:
old_ids = set(old.ids())
new_ids = set(new.ids())
added_uris = [new[id] for id in new_ids - old_ids]
removed_uris = [old[id] for id in old_ids - new_ids]
modified_uris = dict((id, modified) for id, modified in ((id, new[id].get_modified()) for id in new_ids & old_ids) if modified)
for uri in added_uris:
uri.get_modified() # reset the dirty flag of the added uris and sync their old and new values
try:
if added_uris or removed_uris or modified_uris:
return ModifiedList(added=added_uris, removed=removed_uris, modified=modified_uris)
else:
return None
finally:
self.oldvalues[obj] = ContactURIList(new)
def get_old(self, obj):
with self.lock:
return self.oldvalues[obj]
def undo(self, obj):
with self.lock:
self.values[obj] = ContactURIList(self.oldvalues[obj])
class ContactURIList(object):
def __new__(cls, uris=None):
instance = object.__new__(cls)
instance.lock = Lock()
return instance
def __init__(self, uris=None):
self.uris = dict((uri.id, uri) for uri in uris or [])
def __getitem__(self, key):
return self.uris[key]
def __contains__(self, key):
return key in self.uris
def __iter__(self):
return iter(sorted(self.uris.values(), key=attrgetter('id')))
def __reversed__(self):
return iter(sorted(self.uris.values(), key=attrgetter('id'), reverse=True))
__hash__ = None
def __len__(self):
return len(self.uris)
def __eq__(self, other):
if isinstance(other, ContactURIList):
return self.uris == other.uris
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, sorted(self.uris.values(), key=attrgetter('id')))
def __getstate__(self):
with self.lock:
return ItemContainer((id, uri.__getstate__()) for id, uri in self.uris.iteritems())
def __setstate__(self, value):
with self.lock:
self.uris = dict((id, ContactURI(id, **dict((str(key), val) for key, val in state.iteritems()))) for id, state in value.iteritems()) # python < 2.6.5 needs string keyword args -Dan
def ids(self):
return sorted(self.uris.keys())
def add(self, uri):
with self.lock:
self.uris[uri.id] = uri
def remove(self, uri):
with self.lock:
self.uris.pop(uri.id, None)
class ContactURI(SettingsState):
__id__ = SettingsObjectImmutableID(type=ID)
id = __id__
uri = Setting(type=unicode, default='')
type = Setting(type=unicode, default=None, nillable=True)
def __new__(cls, id=None, **state):
if id is None:
id = unique_id()
elif not isinstance(id, basestring):
raise TypeError("id needs to be a string or unicode object")
instance = SettingsState.__new__(cls)
instance.__id__ = id
instance.__setstate__(state)
return instance
def __repr__(self):
return "%s(id=%r)" % (self.__class__.__name__, self.id)
def __toxcap__(self):
attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting))
return XCAPContactURI(self.id, self.uri, self.type, **attributes)
@classmethod
def register_extension(cls, extension):
"""
Register an extension for this class. All Settings and SettingsGroups
defined in the extension will be added to this class, overwriting any
attributes with the same name. Other attributes in the extension are
ignored.
"""
if not issubclass(extension, ContactURIExtension):
raise TypeError("expected subclass of ContactURIExtension, got %r" % (extension,))
for name in dir(extension):
attribute = getattr(extension, name, None)
if isinstance(attribute, SharedSetting):
if SharedSetting.__namespace__ is None:
raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace")
XCAPContactURI.__attributes__.add(name)
if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)):
setattr(cls, name, attribute)
class ContactURIExtension(object):
"""Base class for extensions of ContactURIs"""
def __new__(cls, *args, **kw):
raise TypeError("ContactURIExtension subclasses cannot be instantiated")
class DialogSettings(SettingsGroup):
policy = Setting(type=PolicyValue, default='default')
subscribe = Setting(type=bool, default=False)
class PresenceSettings(SettingsGroup):
policy = Setting(type=PolicyValue, default='default')
subscribe = Setting(type=bool, default=False)
class Contact(SettingsState):
__key__ = AddressbookKey('Contacts')
__id__ = SettingsObjectImmutableID(type=ID)
id = __id__
name = Setting(type=unicode, default='')
uris = ContactURIListDescriptor()
dialog = DialogSettings
presence = PresenceSettings
def __new__(cls, id=None):
with AddressbookManager.load.lock:
if not AddressbookManager.load.called:
raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__)
if id is None:
id = unique_id()
elif not isinstance(id, basestring):
raise TypeError("id needs to be a string or unicode object")
instance = SettingsState.__new__(cls)
instance.__id__ = id
instance.__state__ = 'new'
instance.__xcapcontact__ = None
configuration = ConfigurationManager()
try:
data = configuration.get(instance.__key__)
except ObjectNotFoundError:
pass
else:
instance.__setstate__(data)
instance.__state__ = 'loaded'
instance.__xcapcontact__ = instance.__toxcap__()
return instance
def __establish__(self):
if self.__state__ == 'loaded':
self.__state__ = 'active'
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookContactWasActivated', sender=self, data=TimestampedNotificationData())
def __repr__(self):
return "%s(id=%r)" % (self.__class__.__name__, self.id)
def __toxcap__(self):
contact_uris = [uri.__toxcap__() for uri in self.uris]
dialog_handling = xcap.EventHandling(self.dialog.policy, self.dialog.subscribe)
presence_handling = xcap.EventHandling(self.presence.policy, self.presence.subscribe)
attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting))
return XCAPContact(self.id, self.name, contact_uris, presence_handling, dialog_handling, **attributes)
@run_in_thread('file-io')
def _internal_save(self, originator):
if self.__state__ == 'deleted':
return
modified_settings = self.get_modified()
if not modified_settings and self.__state__ != 'new':
return
account_manager = AccountManager()
configuration = ConfigurationManager()
notification_center = NotificationCenter()
if originator is Local:
originator_account = None
previous_xcapcontact = self.__xcapcontact__
else:
originator_account = originator.account
previous_xcapcontact = originator.xcap_object
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
self.__xcapcontact__ = self.__toxcap__()
if self.__state__ == 'new':
configuration.update(self.__key__, self.__getstate__())
self.__state__ = 'active'
for account in (account for account in xcap_accounts if account is not originator_account):
account.xcap_manager.add_contact(self.__xcapcontact__)
modified_data = None
notification_center.post_notification('AddressbookContactWasActivated', sender=self, data=TimestampedNotificationData())
notification_center.post_notification('AddressbookContactWasCreated', sender=self, data=TimestampedNotificationData())
else:
configuration.update(self.__key__, self.__getstate__())
contact_attributes = self.__xcapcontact__.get_modified(modified_settings)
if 'uris' in modified_settings:
xcap_uris = self.__xcapcontact__.uris
added_uris = [xcap_uris[uri.id] for uri in modified_settings['uris'].added]
removed_uris = [uri.__toxcap__() for uri in modified_settings['uris'].removed]
modified_uris = dict((xcap_uris[id], xcap_uris[id].get_modified(changemap)) for id, changemap in modified_settings['uris'].modified.iteritems())
else:
added_uris = []
removed_uris = []
modified_uris = {}
if self.__xcapcontact__ != previous_xcapcontact:
outofsync_accounts = xcap_accounts
elif originator is Local:
outofsync_accounts = []
else:
outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account)
with MultiAccountTransaction(outofsync_accounts):
for account in outofsync_accounts:
xcap_manager = account.xcap_manager
for xcapuri in added_uris:
xcap_manager.add_contact_uri(self.__xcapcontact__, xcapuri)
for xcapuri in removed_uris:
xcap_manager.remove_contact_uri(self.__xcapcontact__, xcapuri)
for xcapuri, uri_attributes in modified_uris.iteritems():
xcap_manager.update_contact_uri(self.__xcapcontact__, xcapuri, uri_attributes)
if contact_attributes:
xcap_manager.update_contact(self.__xcapcontact__, contact_attributes)
notification_center.post_notification('AddressbookContactDidChange', sender=self, data=TimestampedNotificationData(modified=modified_settings))
modified_data = modified_settings
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='save', modified=modified_data, exception=e))
@run_in_thread('file-io')
def _internal_delete(self, originator):
if self.__state__ == 'deleted':
return
self.__state__ = 'deleted'
configuration = ConfigurationManager()
account_manager = AccountManager()
addressbook_manager = AddressbookManager()
notification_center = NotificationCenter()
if originator is Local:
originator_account = None
else:
originator_account = originator.account
configuration.delete(self.__key__)
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
with MultiAccountTransaction(xcap_accounts):
for group in (group for group in addressbook_manager.get_groups() if self.id in group.contacts):
group.contacts.remove(self)
group.save()
for account in (account for account in xcap_accounts if account is not originator_account):
account.xcap_manager.remove_contact(self.__xcapcontact__)
notification_center.post_notification('AddressbookContactWasDeleted', sender=self, data=TimestampedNotificationData())
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='delete', exception=e))
def save(self):
"""
Store the contact into persistent storage (local and xcap).
This method will post the AddressbookContactWasCreated and
AddressbookContactWasActivated notifications on the first save or
a AddressbookContactDidChange notification on subsequent saves,
regardless of whether the contact has been saved to persistent
storage or not. A CFGManagerSaveFailed notification is posted
if saving to the persistent configuration storage fails.
"""
self._internal_save(originator=Local)
def delete(self):
"""Remove the contact from the persistent storage."""
self._internal_delete(originator=Local)
def clone(self, new_id=None):
"""Create a copy of this contact and all its sub-settings."""
raise NotImplementedError
@classmethod
def register_extension(cls, extension):
"""
Register an extension for this class. All Settings and SettingsGroups
defined in the extension will be added to this class, overwriting any
attributes with the same name. Other attributes in the extension are
ignored.
"""
if not issubclass(extension, ContactExtension):
raise TypeError("expected subclass of ContactExtension, got %r" % (extension,))
for name in dir(extension):
attribute = getattr(extension, name, None)
if isinstance(attribute, SharedSetting):
if SharedSetting.__namespace__ is None:
raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace")
XCAPContact.__attributes__.add(name)
if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)):
setattr(cls, name, attribute)
class ContactExtension(object):
"""Base class for extensions of Contacts"""
def __new__(cls, *args, **kw):
raise TypeError("ContactExtension subclasses cannot be instantiated")
class Policy(SettingsState):
__key__ = AddressbookKey('Policies')
__id__ = SettingsObjectImmutableID(type=ID)
id = __id__
uri = Setting(type=unicode, default='')
name = Setting(type=unicode, default='')
dialog = DialogSettings
presence = PresenceSettings
def __new__(cls, id=None):
with AddressbookManager.load.lock:
if not AddressbookManager.load.called:
raise RuntimeError("cannot instantiate %s before calling AddressbookManager.load" % cls.__name__)
if id is None:
id = unique_id()
elif not isinstance(id, basestring):
raise TypeError("id needs to be a string or unicode object")
instance = SettingsState.__new__(cls)
instance.__id__ = id
instance.__state__ = 'new'
instance.__xcappolicy__ = None
configuration = ConfigurationManager()
try:
data = configuration.get(instance.__key__)
except ObjectNotFoundError:
pass
else:
instance.__setstate__(data)
instance.__state__ = 'loaded'
instance.__xcappolicy__ = instance.__toxcap__()
return instance
def __establish__(self):
if self.__state__ == 'loaded':
self.__state__ = 'active'
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookPolicyWasActivated', sender=self, data=TimestampedNotificationData())
def __repr__(self):
return "%s(id=%r)" % (self.__class__.__name__, self.id)
def __toxcap__(self):
dialog_handling = xcap.EventHandling(self.dialog.policy, self.dialog.subscribe)
presence_handling = xcap.EventHandling(self.presence.policy, self.presence.subscribe)
attributes = dict((name, getattr(self, name)) for name, attr in vars(self.__class__).iteritems() if isinstance(attr, SharedSetting))
return XCAPPolicy(self.id, self.uri, self.name, presence_handling, dialog_handling, **attributes)
@run_in_thread('file-io')
def _internal_save(self, originator):
if self.__state__ == 'deleted':
return
modified_settings = self.get_modified()
if not modified_settings and self.__state__ != 'new':
return
account_manager = AccountManager()
configuration = ConfigurationManager()
notification_center = NotificationCenter()
if originator is Local:
originator_account = None
previous_xcappolicy = self.__xcappolicy__
else:
originator_account = originator.account
previous_xcappolicy = originator.xcap_object
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
self.__xcappolicy__ = self.__toxcap__()
if self.__state__ == 'new':
configuration.update(self.__key__, self.__getstate__())
self.__state__ = 'active'
for account in (account for account in xcap_accounts if account is not originator_account):
account.xcap_manager.add_policy(self.__xcappolicy__)
modified_data = None
notification_center.post_notification('AddressbookPolicyWasActivated', sender=self, data=TimestampedNotificationData())
notification_center.post_notification('AddressbookPolicyWasCreated', sender=self, data=TimestampedNotificationData())
else:
configuration.update(self.__key__, self.__getstate__())
attributes = self.__xcappolicy__.get_modified(modified_settings)
if self.__xcappolicy__ != previous_xcappolicy:
outofsync_accounts = xcap_accounts
elif originator is Local:
outofsync_accounts = []
else:
outofsync_accounts = list(account for account in xcap_accounts if account is not originator_account)
for account in outofsync_accounts:
account.xcap_manager.update_policy(self.__xcappolicy__, attributes)
notification_center.post_notification('AddressbookPolicyDidChange', sender=self, data=TimestampedNotificationData(modified=modified_settings))
modified_data = modified_settings
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='save', modified=modified_data, exception=e))
@run_in_thread('file-io')
def _internal_delete(self, originator):
if self.__state__ == 'deleted':
return
self.__state__ = 'deleted'
configuration = ConfigurationManager()
account_manager = AccountManager()
notification_center = NotificationCenter()
if originator is Local:
originator_account = None
else:
originator_account = originator.account
configuration.delete(self.__key__)
for account in (account for account in account_manager.iter_accounts() if hasattr(account, 'xcap') and account.xcap.discovered and account is not originator_account):
account.xcap_manager.remove_policy(self.__xcappolicy__)
notification_center.post_notification('AddressbookPolicyWasDeleted', sender=self, data=TimestampedNotificationData())
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='delete', exception=e))
def save(self):
"""
Store the policy into persistent storage (local and xcap).
It will post the AddressbookPolicyWasCreated and
AddressbookPolicyWasActivated notifications on the first save or
a AddressbookPolicyDidChange notification on subsequent saves,
regardless of whether the policy has been saved to persistent
storage or not. A CFGManagerSaveFailed notification is posted
if saving to the persistent configuration storage fails.
"""
self._internal_save(originator=Local)
def delete(self):
"""Remove the policy from the persistent storage."""
self._internal_delete(originator=Local)
def clone(self, new_id=None):
"""Create a copy of this policy and all its sub-settings."""
raise NotImplementedError
@classmethod
def register_extension(cls, extension):
"""
Register an extension for this class. All Settings and SettingsGroups
defined in the extension will be added to this class, overwriting any
attributes with the same name. Other attributes in the extension are
ignored.
"""
if not issubclass(extension, PolicyExtension):
raise TypeError("expected subclass of PolicyExtension, got %r" % (extension,))
for name in dir(extension):
attribute = getattr(extension, name, None)
if isinstance(attribute, SharedSetting):
if SharedSetting.__namespace__ is None:
raise RuntimeError("cannot use SharedSetting attributes without first calling SharedSetting.set_namespace")
XCAPPolicy.__attributes__.add(name)
if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)):
setattr(cls, name, attribute)
class PolicyExtension(object):
"""Base class for extensions of Policies"""
def __new__(cls, *args, **kw):
raise TypeError("PolicyExtension subclasses cannot be instantiated")
class AddressbookManager(object):
__metaclass__ = Singleton
implements(IObserver)
def __init__(self):
self.contacts = {}
self.groups = {}
self.policies = {}
self.__xcapaddressbook__ = None
notification_center = NotificationCenter()
notification_center.add_observer(self, name='AddressbookContactWasActivated')
notification_center.add_observer(self, name='AddressbookContactWasDeleted')
notification_center.add_observer(self, name='AddressbookGroupWasActivated')
notification_center.add_observer(self, name='AddressbookGroupWasDeleted')
notification_center.add_observer(self, name='AddressbookPolicyWasActivated')
notification_center.add_observer(self, name='AddressbookPolicyWasDeleted')
notification_center.add_observer(self, name='SIPAccountDidDiscoverXCAPSupport')
notification_center.add_observer(self, name='XCAPManagerDidReloadData')
@execute_once
def load(self):
configuration = ConfigurationManager()
# temporary workaround to migrate contacts to the new format. to be removed later. -Dan
if 'Contacts' in configuration.data or 'ContactGroups' in configuration.data:
account_manager = AccountManager()
old_data = dict(contacts=configuration.data.pop('Contacts', {}), groups=configuration.data.pop('ContactGroups', {}))
if any(account.enabled and account.xcap.enabled and account.xcap.discovered for account in account_manager.get_accounts() if hasattr(account, 'xcap')):
self.__old_data = old_data
else:
self.__migrate_contacts(old_data)
return
[Contact(id=id) for id in configuration.get_names(Contact.__key__)]
[Group(id=id) for id in configuration.get_names(Group.__key__)]
[Policy(id=id) for id in configuration.get_names(Policy.__key__)]
def start(self):
pass
def stop(self):
pass
def has_contact(self, id):
return id in self.contacts
def get_contact(self, id):
return self.contacts[id]
def get_contacts(self):
return self.contacts.values()
def has_group(self, id):
return id in self.groups
def get_group(self, id):
return self.groups[id]
def get_groups(self):
return self.groups.values()
def has_policy(self, id):
return id in self.policies
def get_policy(self, id):
return self.policies[id]
def get_policies(self):
return self.policies.values()
def transaction(self):
account_manager = AccountManager()
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
return MultiAccountTransaction(xcap_accounts)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_AddressbookContactWasActivated(self, notification):
contact = notification.sender
self.contacts[contact.id] = contact
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookManagerDidAddContact', sender=self, data=TimestampedNotificationData(contact=contact))
def _NH_AddressbookContactWasDeleted(self, notification):
contact = notification.sender
del self.contacts[contact.id]
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookManagerDidRemoveContact', sender=self, data=TimestampedNotificationData(contact=contact))
def _NH_AddressbookGroupWasActivated(self, notification):
group = notification.sender
self.groups[group.id] = group
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookManagerDidAddGroup', sender=self, data=TimestampedNotificationData(group=group))
def _NH_AddressbookGroupWasDeleted(self, notification):
group = notification.sender
del self.groups[group.id]
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookManagerDidRemoveGroup', sender=self, data=TimestampedNotificationData(group=group))
def _NH_AddressbookPolicyWasActivated(self, notification):
policy = notification.sender
self.policies[policy.id] = policy
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookManagerDidAddPolicy', sender=self, data=TimestampedNotificationData(policy=policy))
def _NH_AddressbookPolicyWasDeleted(self, notification):
policy = notification.sender
del self.policies[policy.id]
notification_center = NotificationCenter()
notification_center.post_notification('AddressbookManagerDidRemovePolicy', sender=self, data=TimestampedNotificationData(policy=policy))
@run_in_thread('file-io')
def _NH_SIPAccountDidDiscoverXCAPSupport(self, notification):
xcap_manager = notification.sender.xcap_manager
with xcap_manager.transaction():
for contact in self.contacts.values():
xcap_manager.add_contact(contact.__xcapcontact__)
for group in self.groups.values():
xcap_manager.add_group(group.__xcapgroup__)
for policy in self.policies.values():
xcap_manager.add_policy(policy.__xcappolicy__)
@run_in_thread('file-io')
def _NH_XCAPManagerDidReloadData(self, notification):
if notification.data.addressbook == self.__xcapaddressbook__:
return
self.__xcapaddressbook__ = notification.data.addressbook
xcap_manager = notification.sender
xcap_contacts = notification.data.addressbook.contacts
xcap_groups = notification.data.addressbook.groups
xcap_policies = notification.data.addressbook.policies
account_manager = AccountManager()
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
# temporary workaround to migrate contacts to the new format. to be removed later. -Dan
if hasattr(self, '_AddressbookManager__old_data'):
old_data = self.__old_data
del self.__old_data
if not xcap_contacts and not xcap_groups:
self.__migrate_contacts(old_data)
return
with MultiAccountTransaction(xcap_accounts):
# because groups depend on contacts, operation order is add/update contacts, add/update/remove groups & policies, remove contacts -Dan
for xcap_contact in xcap_contacts:
xcap_contact = XCAPContact.normalize(xcap_contact)
try:
contact = self.contacts[xcap_contact.id]
except KeyError:
try:
contact = Contact(xcap_contact.id)
except DuplicateIDError:
log.err()
continue
contact.name = xcap_contact.name
contact.presence.policy = xcap_contact.presence.policy
contact.presence.subscribe = xcap_contact.presence.subscribe
contact.dialog.policy = xcap_contact.dialog.policy
contact.dialog.subscribe = xcap_contact.dialog.subscribe
for name, value in xcap_contact.attributes.iteritems():
setattr(contact, name, value)
for xcap_uri in xcap_contact.uris:
xcap_uri = XCAPContactURI.normalize(xcap_uri)
try:
uri = contact.uris[xcap_uri.id]
except KeyError:
try:
uri = ContactURI(xcap_uri.id)
except DuplicateIDError:
log.err()
continue
contact.uris.add(uri)
uri.uri = xcap_uri.uri
uri.type = xcap_uri.type
for name, value in xcap_uri.attributes.iteritems():
setattr(uri, name, value)
for uri in (uri for uri in list(contact.uris) if uri.id not in xcap_contact.uris):
contact.uris.remove(uri)
contact._internal_save(originator=Remote(xcap_manager.account, xcap_contact))
for xcap_group in xcap_groups:
xcap_group = XCAPGroup.normalize(xcap_group)
try:
group = self.groups[xcap_group.id]
except KeyError:
try:
group = Group(xcap_group.id)
except DuplicateIDError:
log.err()
continue
group.name = xcap_group.name
for name, value in xcap_group.attributes.iteritems():
setattr(group, name, value)
old_contact_ids = set(group.contacts.ids())
new_contact_ids = set(xcap_group.contacts.ids())
for contact in (self.contacts[id] for id in new_contact_ids - old_contact_ids):
group.contacts.add(contact)
for contact in (group.contacts[id] for id in old_contact_ids - new_contact_ids):
group.contacts.remove(contact)
group._internal_save(originator=Remote(xcap_manager.account, xcap_group))
for xcap_policy in xcap_policies:
xcap_policy = XCAPPolicy.normalize(xcap_policy)
try:
policy = self.policies[xcap_policy.id]
except KeyError:
try:
policy = Policy(xcap_policy.id)
except DuplicateIDError:
log.err()
continue
policy.uri = xcap_policy.uri
policy.name = xcap_policy.name
policy.presence.policy = xcap_policy.presence.policy
policy.presence.subscribe = xcap_policy.presence.subscribe
policy.dialog.policy = xcap_policy.dialog.policy
policy.dialog.subscribe = xcap_policy.dialog.subscribe
for name, value in xcap_policy.attributes.iteritems():
setattr(policy, name, value)
policy._internal_save(originator=Remote(xcap_manager.account, xcap_policy))
originator = Remote(xcap_manager.account, None)
for policy in (policy for policy in self.policies.values() if policy.id not in xcap_policies):
policy._internal_delete(originator=originator)
for group in (group for group in self.groups.values() if group.id not in xcap_groups):
group._internal_delete(originator=originator)
for contact in (contact for contact in self.contacts.values() if contact.id not in xcap_contacts):
contact._internal_delete(originator=originator)
def __migrate_contacts(self, old_data):
account_manager = AccountManager()
xcap_accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
with MultiAccountTransaction(xcap_accounts):
# restore the old contacts and groups
old_groups = old_data['groups']
old_contacts = old_data['contacts']
group_idmap = {}
for group_id, group_state in old_groups.iteritems():
group_idmap[group_id] = group = Group()
for name, value in group_state.iteritems():
try:
setattr(group, name, value)
except (ValueError, TypeError):
pass
for account_id, account_contacts in old_contacts.iteritems():
for group_id, contact_map in account_contacts.iteritems():
for uri, contact_data in contact_map.iteritems():
contact = Contact()
for name, value in contact_data.iteritems():
try:
setattr(contact, name, value)
except (ValueError, TypeError):
pass
contact.uris.add(ContactURI(uri=uri))
contact.save()
group = group_idmap.get(group_id, Null)
group.contacts.add(contact)
for group in group_idmap.itervalues():
group.save()
diff --git a/sipsimple/configuration/__init__.py b/sipsimple/configuration/__init__.py
index e26fd74e..a813064e 100644
--- a/sipsimple/configuration/__init__.py
+++ b/sipsimple/configuration/__init__.py
@@ -1,828 +1,829 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""Generic configuration management"""
__all__ = ['ConfigurationManager', 'ConfigurationError', 'ObjectNotFoundError', 'DuplicateIDError', 'DefaultValue',
'AbstractSetting', 'Setting', 'CorrelatedSetting', 'SettingsStateMeta', 'SettingsState', 'SettingsGroup',
'SettingsObjectID', 'SettingsObjectImmutableID', 'SettingsObject', 'SettingsObjectExtension',
'PersistentKey', 'ItemContainer']
from abc import ABCMeta, abstractmethod
from itertools import chain
from threading import Lock
from application import log
from application.notification import NotificationCenter
from application.python.descriptor import isdescriptor
from application.python.types import Singleton
+from application.python.weakref import weakobjectmap
from backports.weakref import WeakSet
from sipsimple.threading import run_in_thread
-from sipsimple.util import TimestampedNotificationData, weakobjectmap
+from sipsimple.util import TimestampedNotificationData
## Exceptions
class ConfigurationError(Exception): pass
class ObjectNotFoundError(ConfigurationError): pass
class DuplicateIDError(ValueError): pass
class PersistentKey(unicode):
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, unicode.__repr__(self))
class ItemContainer(dict):
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, dict.__repr__(self))
## ConfigurationManager
class ConfigurationManager(object):
"""
Singleton class used for storing and retrieving options, organized in
sections. A section contains a list of objects, each with an assigned name
which allows access to the object.
"""
__metaclass__ = Singleton
def __init__(self):
self.backend = None
self.data = None
def start(self):
"""
Initialize the ConfigurationManager to use the specified backend. This
method can only be called once, with an object which provides IBackend.
The other methods of the object cannot be used unless this method was
called.
"""
from sipsimple.application import SIPApplication
from sipsimple.configuration.backend import IConfigurationBackend
if self.backend is not None:
raise RuntimeError("ConfigurationManager already started")
if SIPApplication.storage is None:
raise RuntimeError("SIPApplication.storage must be defined before starting the ConfigurationManager")
backend = SIPApplication.storage.configuration_backend
if not IConfigurationBackend.providedBy(backend):
raise TypeError("SIPApplication.storage.configuration_backend must implement the IConfigurationBackend interface")
self.data = backend.load()
self.backend = backend
def update(self, key, data):
"""
Save the object's data under the tree path specified by key (a list
of strings). Cannot be called before start().
"""
if self.backend is None:
raise RuntimeError("ConfigurationManager cannot be used unless started")
if not key:
raise KeyError("key cannot be empty")
self._update(self.data, list(key), data)
def rename(self, old_key, new_key):
"""
Rename the object identified by old_key to new_key (list of strings).
Cannot be called before start().
"""
if self.backend is None:
raise RuntimeError("ConfigurationManager cannot be used unless started")
if not old_key or not new_key:
raise KeyError("old_key and/or new_key cannot be empty")
try:
data = self._pop(self.data, list(old_key))
except KeyError:
raise ObjectNotFoundError("object %s does not exist" % '/'.join(old_key))
self._insert(self.data, list(new_key), data)
def delete(self, key):
"""
Delete the object in the tree path specified by key (list of strings).
Cannot be called before start().
"""
if self.backend is None:
raise RuntimeError("ConfigurationManager cannot be used unless started")
if not key:
raise KeyError("key cannot be empty")
try:
self._pop(self.data, list(key))
except KeyError:
pass
def get(self, key):
"""
Get the object in the tree path specified by key (list of strings).
Raises ObjectNotFoundError if the object does not exist. Cannot be
called before start().
"""
if self.backend is None:
raise RuntimeError("ConfigurationManager cannot be used unless started")
if not key:
raise KeyError("key cannot be empty")
try:
return self._get(self.data, list(key))
except KeyError:
raise ObjectNotFoundError("object %s does not exist" % '/'.join(key))
def get_names(self, key):
"""
Get all the names under the specified key (a list of strings).
Returns a list containing the names. Cannot be called before start().
"""
if self.backend is None:
raise RuntimeError("ConfigurationManager cannot be used unless started")
if not key:
raise KeyError("key cannot be empty")
try:
data = self._get(self.data, list(key))
return data.keys()
except KeyError:
return []
def save(self):
"""
Flush the modified objects. Cannot be called before start().
"""
if self.backend is None:
raise RuntimeError("ConfigurationManager cannot be used unless started")
self.backend.save(self.data)
def _get(self, data_tree, key):
subtree_key = key.pop(0)
data_subtree = data_tree[subtree_key]
if key:
return self._get(data_subtree, key)
else:
return data_subtree
def _insert(self, data_tree, key, data):
subtree_key = key.pop(0)
data_subtree = data_tree.setdefault(subtree_key, {})
if key:
self._insert(data_subtree, key, data)
else:
data_subtree.update(data)
def _pop(self, data_tree, key):
subtree_key = key.pop(0)
data_subtree = data_tree[subtree_key]
if key:
data = self._pop(data_subtree, key)
if not isinstance(subtree_key, PersistentKey) and not data_subtree:
del data_tree[subtree_key]
return data
else:
return data_tree.pop(subtree_key)
def _update(self, data_tree, key, data):
subtree_key = key.pop(0)
data_subtree = data_tree.setdefault(subtree_key, {})
if key:
self._update(data_subtree, key, data)
else:
self._update_dict(data_subtree, data)
if not isinstance(subtree_key, PersistentKey) and not data_subtree:
del data_tree[subtree_key]
def _update_dict(self, old_data, new_data):
for key, value in new_data.iteritems():
if value is DefaultValue:
old_data.pop(key, None)
elif isinstance(value, ItemContainer):
if key not in old_data or type(old_data[key]) is not dict:
old_data[key] = {}
key_subtree = old_data[key]
for removed_key in set(key_subtree) - set(value):
del key_subtree[removed_key]
self._update_dict(key_subtree, value)
if not key_subtree:
del old_data[key]
elif type(value) is dict:
if key in old_data and type(old_data[key]) is not dict:
del old_data[key]
self._update_dict(old_data.setdefault(key, {}), value)
if not old_data[key]:
del old_data[key]
else:
old_data[key] = value
## Descriptors and base classes used for represeting configuration settings
class DefaultValue(object):
"""
This object can be set as the value for a setting and it will reset the
setting to the default value.
"""
class ModifiedValue(object):
"""
Instances of this class represent the state (the old and new values) of
settings.
"""
__slots__ = ('old', 'new')
def __init__(self, old, new):
self.old = old
self.new = new
def __repr__(self):
return '%s(old=%r, new=%r)' % (self.__class__.__name__, self.old, self.new)
class SettingsObjectID(object):
"""
Descriptor for dynamic configuration object IDs.
"""
def __init__(self, type):
self.type = type
self.values = weakobjectmap()
self.oldvalues = weakobjectmap()
self.dirty = weakobjectmap()
self.lock = Lock()
def __get__(self, obj, objtype):
return self if obj is None else self.values[obj]
def __set__(self, obj, value):
with self.lock:
if not isinstance(value, self.type):
value = self.type(value)
if obj in self.values and self.values[obj] == value:
return
if obj in self.oldvalues and self.oldvalues[obj] == value:
self.values[obj] = self.oldvalues[obj]
self.dirty[obj] = False
return
try:
other_obj = (key for key, val in chain(self.values.iteritems(), self.oldvalues.iteritems()) if val==value).next()
except StopIteration:
pass
else:
raise DuplicateIDError('SettingsObject ID already used by another %s' % other_obj.__class__.__name__)
if obj in self.values:
self.values[obj] = value
self.dirty[obj] = True
else:
self.values[obj] = self.oldvalues[obj] = value
self.dirty[obj] = False
def __delete__(self, obj):
raise AttributeError('cannot delete attribute')
def get_modified(self, obj):
"""
Returns a ModifiedValue instance with references to the old and new
values or None if not modified.
"""
with self.lock:
try:
if self.dirty.get(obj, False):
return ModifiedValue(old=self.oldvalues[obj], new=self.values[obj])
else:
return None
finally:
self.oldvalues[obj] = self.values[obj]
self.dirty[obj] = False
def get_old(self, obj):
return self.oldvalues[obj]
def undo(self, obj):
with self.lock:
self.values[obj] = self.oldvalues[obj]
self.dirty[obj] = False
class SettingsObjectImmutableID(object):
"""
Descriptor for immutable runtime allocated configuration object IDs.
"""
def __init__(self, type):
self.type = type
self.values = weakobjectmap()
self.lock = Lock()
def __get__(self, obj, objtype):
return self if obj is None else self.values[obj]
def __set__(self, obj, value):
with self.lock:
if obj in self.values:
raise AttributeError('attribute is read-only')
if not isinstance(value, self.type):
value = self.type(value)
try:
other_obj = (key for key, val in self.values.iteritems() if val==value).next()
except StopIteration:
pass
else:
raise DuplicateIDError('SettingsObject ID already used by another %s' % other_obj.__class__.__name__)
self.values[obj] = value
def __delete__(self, obj):
raise AttributeError('cannot delete attribute')
class AbstractSetting(object):
"""Abstract base class for setting type descriptors"""
__metaclass__ = ABCMeta
@abstractmethod
def __get__(self, obj, objtype):
raise NotImplementedError
@abstractmethod
def __set__(self, obj, value):
raise NotImplementedError
def __delete__(self, obj):
raise AttributeError('cannot delete attribute')
@abstractmethod
def __getstate__(self, obj):
raise NotImplementedError
@abstractmethod
def __setstate__(self, obj, value):
raise NotImplementedError
@abstractmethod
def get_modified(self, obj):
raise NotImplementedError
@abstractmethod
def get_old(self, obj):
raise NotImplementedError
@abstractmethod
def undo(self, obj):
raise NotImplementedError
class Setting(AbstractSetting):
"""
Descriptor represeting a setting in a configuration object.
If a setting is set to the object DefaultValue, it will be reset to the
default. Also, only Setting attributes with nillable=True can be assigned
the value None. All other values are passed to the type specified.
"""
def __init__(self, type, default=None, nillable=False):
if default is None and not nillable:
raise TypeError("default must be specified if object is not nillable")
self.type = type
self.default = default
self.nillable = nillable
self.values = weakobjectmap()
self.oldvalues = weakobjectmap()
self.dirty = weakobjectmap()
self.lock = Lock()
def __get__(self, obj, objtype):
if obj is None:
return self
return self.values.get(obj, self.default)
def __set__(self, obj, value):
with self.lock:
if value is None and not self.nillable:
raise ValueError("setting attribute is not nillable")
if value is DefaultValue:
if obj in self.values:
self.values.pop(obj)
self.dirty[obj] = obj in self.oldvalues
return
if value is not None and not isinstance(value, self.type):
value = self.type(value)
if obj in self.values and self.values[obj] == value:
return
self.values[obj] = value
self.dirty[obj] = value != self.oldvalues.get(obj, DefaultValue)
def __getstate__(self, obj):
value = self.values.get(obj, DefaultValue)
if value in (None, DefaultValue):
pass
elif issubclass(self.type, bool):
value = u'true' if value else u'false'
elif issubclass(self.type, (int, long, basestring)):
value = unicode(value)
else:
try:
value = value.__getstate__()
except AttributeError:
raise TypeError("Setting type %s does not provide __getstate__" % value.__class__.__name__)
return value
def __setstate__(self, obj, value):
with self.lock:
if value is None and not self.nillable:
raise ValueError("setting attribute is not nillable")
if value is None:
pass
elif issubclass(self.type, bool):
if value.lower() in ('true', 'yes', 'on', '1'):
value = True
elif value.lower() in ('false', 'no', 'off', '0'):
value = False
else:
raise ValueError("invalid boolean value: %s" % (value,))
elif issubclass(self.type, (int, long, basestring)):
value = self.type(value)
else:
object = self.type.__new__(self.type)
object.__setstate__(value)
value = object
self.oldvalues[obj] = self.values[obj] = value
self.dirty[obj] = False
def get_modified(self, obj):
"""
Returns a ModifiedValue instance with references to the old and new
values or None if not modified.
"""
with self.lock:
try:
if self.dirty.get(obj, False):
return ModifiedValue(old=self.oldvalues.get(obj, self.default), new=self.values.get(obj, self.default))
else:
return None
finally:
try:
self.oldvalues[obj] = self.values[obj]
except KeyError:
self.oldvalues.pop(obj, None)
self.dirty[obj] = False
def get_old(self, obj):
return self.oldvalues.get(obj, self.default)
def undo(self, obj):
with self.lock:
if obj in self.oldvalues:
self.values[obj] = self.oldvalues[obj]
else:
self.values.pop(obj, None)
self.dirty[obj] = False
class CorrelatedSetting(Setting):
"""
Descriptor represeting a setting in a configuration object that is
correlated with another setting on the same configuration object.
Sibling is the name of the sibling setting and validator is a callable
that will receive the setting value and the sibling setting value and
should raise an exception if the setting value is not acceptable relative
to the sibling setting value.
If a setting is set to the object DefaultValue, it will be reset to the
default. Also, only Setting attributes with nillable=True can be assigned
the value None. All other values are passed to the type specified.
"""
correlation_lock = Lock()
def __init__(self, type, sibling, validator, default=None, nillable=False):
Setting.__init__(self, type, default, nillable)
self.sibling = sibling
self.validator = validator
def __set__(self, obj, value):
with self.correlation_lock:
sibling_value = getattr(obj, self.sibling)
self.validator(value, sibling_value)
Setting.__set__(self, obj, value)
class SettingsStateMeta(type):
__established__ = WeakSet()
def __call__(cls, *args, **kw):
instance = super(SettingsStateMeta, cls).__call__(*args, **kw)
if hasattr(instance, '__establish__') and instance not in cls.__established__:
cls.__established__.add(instance)
instance.__establish__()
return instance
class SettingsState(object):
"""
This class represents configuration objects which can be saved and restored.
"""
__metaclass__ = SettingsStateMeta
def get_modified(self):
"""
Returns a dictionary containing the settings which have been changed.
The keys are the full paths to the attributes (from this object), which
are mapped to a ModifiedValue instance with references to the old and
new values.
"""
modified = {}
for name in dir(self.__class__):
attribute = getattr(self.__class__, name, None)
if isinstance(attribute, SettingsGroupMeta):
modified_settings = getattr(self, name).get_modified()
modified.update(dict((name+'.'+k if k else name, v) for k,v in modified_settings.iteritems()))
elif isinstance(attribute, AbstractSetting):
modified_value = attribute.get_modified(self)
if modified_value is not None:
modified[name] = modified_value
return modified
def clone(self):
"""
Create a copy of this object and all its sub settings.
"""
raise NotImplementedError
def update(self, object):
"""
Update the settings and subsettings of this settings object using the
ones in the specified object.
"""
raise NotImplementedError
def __getstate__(self):
state = {}
for name in dir(self.__class__):
attribute = getattr(self.__class__, name, None)
if isinstance(attribute, SettingsGroupMeta):
state[name] = getattr(self, name).__getstate__()
elif isinstance(attribute, AbstractSetting):
state[name] = attribute.__getstate__(self)
return state
def __setstate__(self, state):
configuration_manager = ConfigurationManager()
notification_center = NotificationCenter()
for name, value in state.iteritems():
attribute = getattr(self.__class__, name, None)
if isinstance(attribute, SettingsGroupMeta):
group = getattr(self, name)
try:
group.__setstate__(value)
except ValueError, e:
notification_center.post_notification('CFGManagerLoadFailed', sender=configuration_manager, data=TimestampedNotificationData(attribute=name, container=self, error=e))
elif isinstance(attribute, AbstractSetting):
try:
attribute.__setstate__(self, value)
except ValueError, e:
notification_center.post_notification('CFGManagerLoadFailed', sender=configuration_manager, data=TimestampedNotificationData(attribute=name, container=self, error=e))
class SettingsGroupMeta(SettingsStateMeta):
"""
Metaclass for SettingsGroup and its subclasses which allows them to be used
as descriptor instances.
"""
def __init__(cls, name, bases, dct):
super(SettingsGroupMeta, cls).__init__(name, bases, dct)
cls.values = weakobjectmap()
def __get__(cls, obj, objtype):
if obj is None:
return cls
try:
return cls.values[obj]
except KeyError:
return cls.values.setdefault(obj, cls())
def __set__(cls, obj, value):
raise AttributeError("cannot overwrite group of settings")
def __delete__(self, obj):
raise AttributeError('cannot delete group of settings')
class SettingsGroup(SettingsState):
"""
Base class for settings groups, i.e. non-leaf and non-root nodes in the
configuration tree. All SettingsGroup subclasses are descriptor instances
which return an instance of the subclass type when accessed. All
SettingsGroup intances are created without passing any arguments to the
constructor.
class ContainedGroup(SettingsGroup):
pass
class ContainingGroup(SettingsGroup):
subgroup = ContainedGroup
"""
__metaclass__ = SettingsGroupMeta
class ConditionalSingleton(type):
"""A conditional singleton based on cls.__id__ being static or not"""
lock = Lock()
def __init__(cls, name, bases, dic):
super(ConditionalSingleton, cls).__init__(name, bases, dic)
cls.__instance__ = None
def __call__(cls, *args, **kw):
if isinstance(cls.__id__, basestring):
if args or kw:
raise TypeError("cannot have arguments for %s because it is a singleton" % cls.__name__)
with cls.lock:
if cls.__instance__ is None:
cls.__instance__ = super(ConditionalSingleton, cls).__call__(*args, **kw)
return cls.__instance__
else:
return super(ConditionalSingleton, cls).__call__(*args, **kw)
class SettingsObjectMeta(SettingsStateMeta, ConditionalSingleton):
"""Metaclass to singleton-ize SettingsObject subclasses with static ids"""
def __init__(cls, name, bases, dic):
if not (cls.__id__ is None or isinstance(cls.__id__, basestring) or isdescriptor(cls.__id__)):
raise TypeError("%s.__id__ must be None, a string instance or a descriptor" % name)
super(SettingsObjectMeta, cls).__init__(name, bases, dic)
class SettingsObject(SettingsState):
"""
Subclass for top-level configuration objects. These objects are identifiable
by either a global id (set in the __id__ attribute of the class) or a local
id passed as the sole argument when instantiating SettingsObjects.
For SettingsObject subclasses which are meant to be used exclusively with a
local id, the class attribute __id__ should be left to the value None; if
__init__ is defined, it would have to accept exactly one argument: id.
The local id takes precedence over the one specified as a class attribute.
Note: __init__ and __new__ will be called not only when a new object is
created (i.e. there weren't any settings saved in the configuration), but
also when the object is retrieved from the configuration.
"""
__metaclass__ = SettingsObjectMeta
__group__ = None
__id__ = None
def __new__(cls, id=None):
id = id or cls.__id__
if id is None:
raise ValueError("id is required for instantiating %s" % cls.__name__)
if not isinstance(id, basestring):
raise TypeError("id needs to be a string instance")
configuration = ConfigurationManager()
instance = SettingsState.__new__(cls)
instance.__id__ = id
instance.__state__ = 'new'
try:
data = configuration.get(instance.__key__)
except ObjectNotFoundError:
pass
else:
instance.__setstate__(data)
instance.__state__ = 'loaded'
return instance
def __establish__(self):
if self.__state__ == 'loaded' or self.__instance__ is not None:
self.__state__ = 'active'
notification_center = NotificationCenter()
notification_center.post_notification('CFGSettingsObjectWasActivated', sender=self, data=TimestampedNotificationData())
@property
def __key__(self):
if isinstance(self.__class__.__id__, (SettingsObjectID, SettingsObjectImmutableID)):
id_key = PersistentKey(self.__id__)
else:
id_key = unicode(self.__id__)
if self.__group__ is not None:
return [self.__group__, id_key]
else:
return [id_key]
@property
def __oldkey__(self):
if isinstance(self.__class__.__id__, SettingsObjectID):
id_key = PersistentKey(self.__class__.__id__.get_old(self))
elif isinstance(self.__class__.__id__, SettingsObjectImmutableID):
id_key = PersistentKey(self.__id__)
else:
id_key = unicode(self.__id__)
if self.__group__ is not None:
return [self.__group__, id_key]
else:
return [id_key]
@run_in_thread('file-io')
def save(self):
"""
Use the ConfigurationManager to store the object under its id in the
specified group or top-level otherwise, depending on whether group is
None.
This method will also post a CFGSettingsObjectDidChange notification,
regardless of whether the settings have been saved to persistent storage
or not. If the save does fail, a CFGManagerSaveFailed notification is
posted as well.
"""
if self.__state__ == 'deleted':
return
configuration = ConfigurationManager()
notification_center = NotificationCenter()
oldkey = self.__oldkey__ # save this here as get_modified will reset it
modified_id = self.__class__.__id__.get_modified(self) if isinstance(self.__class__.__id__, SettingsObjectID) else None
modified_settings = self.get_modified()
if not modified_id and not modified_settings and self.__state__ != 'new':
return
if self.__state__ == 'new':
configuration.update(self.__key__, self.__getstate__())
self.__state__ = 'active'
notification_center.post_notification('CFGSettingsObjectWasActivated', sender=self, data=TimestampedNotificationData())
notification_center.post_notification('CFGSettingsObjectWasCreated', sender=self, data=TimestampedNotificationData())
modified_data = None
else:
if modified_id:
configuration.rename(oldkey, self.__key__)
if modified_settings:
configuration.update(self.__key__, self.__getstate__())
modified_data = modified_settings or {}
if modified_id:
modified_data['__id__'] = modified_id
notification_center.post_notification('CFGSettingsObjectDidChange', sender=self, data=TimestampedNotificationData(modified=modified_data))
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='save', modified=modified_data, exception=e))
@run_in_thread('file-io')
def delete(self):
"""
Remove this object from the persistent configuration.
"""
if self.__id__ is self.__class__.__id__:
raise TypeError("cannot delete %s instance with default id" % self.__class__.__name__)
if self.__state__ == 'deleted':
return
self.__state__ = 'deleted'
configuration = ConfigurationManager()
notification_center = NotificationCenter()
configuration.delete(self.__oldkey__) # we need the key that wasn't yet saved
notification_center.post_notification('CFGSettingsObjectWasDeleted', sender=self, data=TimestampedNotificationData())
try:
configuration.save()
except Exception, e:
log.err()
notification_center.post_notification('CFGManagerSaveFailed', sender=configuration, data=TimestampedNotificationData(object=self, operation='delete', exception=e))
def clone(self, new_id):
"""
Create a copy of this object and all its sub settings.
"""
raise NotImplementedError
@classmethod
def register_extension(cls, extension):
"""
Register an extension of this SettingsObject. All Settings and
SettingsGroups defined in the extension will be added to this
SettingsObject, overwriting any attributes with the same name.
Other attributes in the extension are ignored.
"""
if not issubclass(extension, SettingsObjectExtension):
raise TypeError("expected subclass of SettingsObjectExtension, got %r" % (extension,))
for name in dir(extension):
attribute = getattr(extension, name, None)
if isinstance(attribute, (AbstractSetting, SettingsGroupMeta)):
setattr(cls, name, attribute)
class SettingsObjectExtension(object):
"""
Base class for extensions of SettingsObjects.
"""
def __new__(self, *args, **kwargs):
raise TypeError("SettingsObjectExtension subclasses cannot be instantiated")
diff --git a/sipsimple/payloads/__init__.py b/sipsimple/payloads/__init__.py
index 9df6336c..f36bf987 100644
--- a/sipsimple/payloads/__init__.py
+++ b/sipsimple/payloads/__init__.py
@@ -1,1191 +1,1193 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
__all__ = ['ParserError',
'BuilderError',
'ValidationError',
'IterateTypes',
'IterateIDs',
'IterateItems',
'All',
'parse_qname',
'XMLDocument',
'XMLAttribute',
'XMLElementID',
'XMLElementChild',
'XMLElementChoiceChild',
'XMLStringChoiceChild',
'XMLElement',
'XMLRootElement',
'XMLSimpleElement',
'XMLStringElement',
'XMLLocalizedStringElement',
'XMLBooleanElement',
'XMLByteElement',
'XMLUnsignedByteElement',
'XMLShortElement',
'XMLUnsignedShortElement',
'XMLIntElement',
'XMLUnsignedIntElement',
'XMLLongElement',
'XMLUnsignedLongElement',
'XMLIntegerElement',
'XMLPositiveIntegerElement',
'XMLNegativeIntegerElement',
'XMLNonNegativeIntegerElement',
'XMLNonPositiveIntegerElement',
'XMLDecimalElement',
'XMLDateTimeElement',
'XMLAnyURIElement',
'XMLEmptyElement',
'XMLEmptyElementRegistryType',
'XMLListElement',
'XMLListRootElement',
'XMLStringListElement']
import os
import sys
import urllib
from collections import defaultdict, deque
from copy import deepcopy
from decimal import Decimal
from itertools import chain, izip
from weakref import WeakValueDictionary
from application.python import Null
from application.python.descriptor import classproperty
+from application.python.types import MarkerType
+from application.python.weakref import weakobjectmap
from lxml import etree
from sipsimple.payloads.datatypes import Boolean, Byte, UnsignedByte, Short, UnsignedShort, Int, UnsignedInt, Long, UnsignedLong
from sipsimple.payloads.datatypes import PositiveInteger, NegativeInteger, NonNegativeInteger, NonPositiveInteger, DateTime, AnyURI
-from sipsimple.util import All, MarkerType, weakobjectmap
+from sipsimple.util import All
## Exceptions
class ParserError(Exception): pass
class BuilderError(Exception): pass
class ValidationError(ParserError): pass
## Markers
class Marker(object):
__metaclass__ = MarkerType
class IterateTypes(Marker): pass
-class IterateIDs(Marker): pass
+class IterateIDs(Marker): pass
class IterateItems(Marker): pass
## Utilities
def parse_qname(qname):
if qname[0] == '{':
qname = qname[1:]
return qname.split('}')
else:
return None, qname
## XMLDocument
class XMLDocumentType(type):
def __init__(cls, name, bases, dct):
cls.nsmap = {}
cls.schema_map = {}
cls.element_map = {}
cls.root_element = None
cls.schema = None
cls.parser = None
for base in reversed(bases):
if hasattr(base, 'element_map'):
cls.element_map.update(base.element_map)
if hasattr(base, 'schema_map'):
cls.schema_map.update(base.schema_map)
if hasattr(base, 'nsmap'):
cls.nsmap.update(base.nsmap)
cls._update_schema()
def _update_schema(cls):
if cls.schema_map:
schema = """<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
%s
</xs:schema>
""" % '\r\n'.join('<xs:import namespace="%s" schemaLocation="%s"/>' % (ns, urllib.quote(schema)) for ns, schema in cls.schema_map.iteritems())
cls.schema = etree.XMLSchema(etree.XML(schema))
cls.parser = etree.XMLParser(schema=cls.schema, remove_blank_text=True)
else:
cls.schema = None
cls.parser = etree.XMLParser(remove_blank_text=True)
class XMLDocument(object):
__metaclass__ = XMLDocumentType
encoding = 'UTF-8'
content_type = None
@classmethod
def parse(cls, document):
try:
if isinstance(document, str):
xml = etree.XML(document, parser=cls.parser)
elif isinstance(document, unicode):
xml = etree.XML(document.encode('utf-8'), parser=cls.parser)
else:
xml = etree.parse(document, parser=cls.parser).getroot()
if cls.schema is not None:
cls.schema.assertValid(xml)
except (etree.DocumentInvalid, etree.XMLSyntaxError), e:
raise ParserError(str(e))
else:
return cls.root_element.from_element(xml, xml_document=cls)
@classmethod
def build(cls, root_element, encoding=None, pretty_print=False, validate=True):
if type(root_element) is not cls.root_element:
raise TypeError("can only build XML documents from root elements of type %s" % cls.root_element.__name__)
element = root_element.to_element()
if validate and cls.schema is not None:
cls.schema.assertValid(element)
# Cleanup namespaces and move element NS mappings to the global scope.
normalized_element = etree.Element(element.tag, attrib=element.attrib, nsmap=dict(chain(element.nsmap.iteritems(), cls.nsmap.iteritems())))
normalized_element.text = element.text
normalized_element.tail = element.tail
normalized_element.extend(deepcopy(child) for child in element)
etree.cleanup_namespaces(normalized_element)
return etree.tostring(normalized_element, encoding=encoding or cls.encoding, method='xml', xml_declaration=True, pretty_print=pretty_print)
@classmethod
def create(cls, build_kw={}, **kw):
return cls.build(cls.root_element(**kw), **build_kw)
@classmethod
def register_element(cls, xml_class):
cls.element_map[xml_class.qname] = xml_class
for child in cls.__subclasses__():
child.register_element(xml_class)
@classmethod
def get_element(cls, qname, default=None):
return cls.element_map.get(qname, default)
@classmethod
def register_namespace(cls, namespace, prefix=None, schema=None):
if prefix in cls.nsmap:
raise ValueError("prefix %s is already registered in %s" % (prefix, cls.__name__))
if namespace in cls.nsmap.itervalues():
raise ValueError("namespace %s is already registered in %s" % (namespace, cls.__name__))
cls.nsmap[prefix] = namespace
if schema is not None:
cls.schema_map[namespace] = os.path.join(os.path.dirname(__file__), 'xml-schemas', schema)
cls._update_schema()
for child in cls.__subclasses__():
child.register_namespace(namespace, prefix, schema)
@classmethod
def unregister_namespace(cls, namespace):
try:
prefix = (prefix for prefix in cls.nsmap if cls.nsmap[prefix]==namespace).next()
except StopIteration:
raise KeyError("namespace %s is not registered in %s" % (namespace, cls.__name__))
del cls.nsmap[prefix]
schema = cls.schema_map.pop(namespace, None)
if schema is not None:
cls._update_schema()
for child in cls.__subclasses__():
try:
child.unregister_namespace(namespace)
except KeyError:
pass
## Children descriptors
class XMLAttribute(object):
def __init__(self, name, xmlname=None, type=unicode, default=None, required=False, test_equal=True, onset=None, ondel=None):
self.name = name
self.xmlname = xmlname or name
self.type = type
self.default = default
self.__xmlparse__ = getattr(type, '__xmlparse__', lambda value: value)
self.__xmlbuild__ = getattr(type, '__xmlbuild__', unicode)
self.required = required
self.test_equal = test_equal
self.onset = onset
self.ondel = ondel
self.values = weakobjectmap()
def __get__(self, obj, objtype):
if obj is None:
return self
try:
return self.values[obj]
except KeyError:
value = self.values.setdefault(obj, self.default)
if value is not None:
obj.element.set(self.xmlname, self.build(value))
return value
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.type):
value = self.type(value)
old_value = self.values.get(obj, self.default)
if value == old_value:
return
if value is not None:
obj.element.set(self.xmlname, self.build(value))
else:
obj.element.attrib.pop(self.xmlname, None)
self.values[obj] = value
obj.__dirty__ = True
if self.onset:
self.onset(obj, self, value)
def __delete__(self, obj):
obj.element.attrib.pop(self.xmlname, None)
try:
value = self.values.pop(obj)
except KeyError:
pass
else:
if value != self.default:
obj.__dirty__ = True
if self.ondel:
self.ondel(obj, self)
def parse(self, xmlvalue):
return self.__xmlparse__(xmlvalue)
def build(self, value):
return self.__xmlbuild__(value)
class XMLElementID(XMLAttribute):
"""An XMLAttribute that represents the ID of an element (immutable)."""
def __set__(self, obj, value):
if obj in self.values:
raise AttributeError("An XML element ID cannot be changed")
super(XMLElementID, self).__set__(obj, value)
def __delete__(self, obj):
raise AttributeError("An XML element ID cannot be deleted")
class XMLElementChild(object):
def __init__(self, name, type, required=False, test_equal=True, onset=None, ondel=None):
self.name = name
self.type = type
self.required = required
self.test_equal = test_equal
self.onset = onset
self.ondel = ondel
self.values = weakobjectmap()
def __get__(self, obj, objtype):
if obj is None:
return self
try:
return self.values[obj]
except KeyError:
return None
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.type):
value = self.type(value)
same_value = False
old_value = self.values.get(obj)
if value is old_value:
return
elif value is not None and value == old_value:
value.__dirty__ = old_value.__dirty__
same_value = True
if old_value is not None:
obj.element.remove(old_value.element)
if value is not None:
obj._insert_element(value.element)
self.values[obj] = value
if not same_value:
obj.__dirty__ = True
if self.onset:
self.onset(obj, self, value)
def __delete__(self, obj):
try:
old_value = self.values.pop(obj)
except KeyError:
pass
else:
if old_value is not None:
obj.element.remove(old_value.element)
obj.__dirty__ = True
if self.ondel:
self.ondel(obj, self)
class XMLElementChoiceChildWrapper(object):
__slots__ = ('descriptor', 'type')
def __init__(self, descriptor, type):
self.descriptor = descriptor
self.type = type
def __getattribute__(self, name):
if name in ('descriptor', 'type', 'register_extension', 'unregister_extension'):
return super(XMLElementChoiceChildWrapper, self).__getattribute__(name)
else:
return self.descriptor.__getattribute__(name)
def __setattr__(self, name, value):
if name in ('descriptor', 'type'):
super(XMLElementChoiceChildWrapper, self).__setattr__(name, value)
else:
setattr(self.descriptor, name, value)
def __dir__(self):
return dir(self.descriptor) + ['descriptor', 'type', 'register_extension', 'unregister_extension']
def register_extension(self, type):
if self.extension_type is None:
raise ValueError("The %s XML choice element of %s does not support extensions" % (self.name, self.type.__name__))
if not issubclass(type, XMLElement) or not issubclass(type, self.extension_type):
raise TypeError("type is not a subclass of XMLElement and/or %s: %s" % (self.extension_type.__name__, type.__name__))
if type in self.types:
raise ValueError("%s is already registered as a choice extension" % type.__name__)
self.types.add(type)
self.type._xml_children_qname_map[type.qname] = (self.descriptor, type)
for child_class in self.type.__subclasses__():
child_class._xml_children_qname_map[type.qname] = (self.descriptor, type)
def unregister_extension(self, type):
if self.extension_type is None:
raise ValueError("The %s XML choice element of %s does not support extensions" % (self.name, self.type.__name__))
try:
self.types.remove(type)
except ValueError:
raise ValueError("%s is not a registered choice extension on %s" % (type.__name__, self.type.__name__))
del self.type._xml_children_qname_map[type.qname]
for child_class in self.type.__subclasses__():
del child_class._xml_children_qname_map[type.qname]
class XMLElementChoiceChild(object):
def __init__(self, name, types, extension_type=None, required=False, test_equal=True, onset=None, ondel=None):
self.name = name
self.types = set(types)
self.extension_type = extension_type
self.required = required
self.test_equal = test_equal
self.onset = onset
self.ondel = ondel
self.values = weakobjectmap()
def __get__(self, obj, objtype):
if obj is None:
return XMLElementChoiceChildWrapper(self, objtype)
try:
return self.values[obj]
except KeyError:
return None
def __set__(self, obj, value):
if value is not None and type(value) not in self.types:
raise TypeError("%s is not an acceptable type for %s" % (value.__class__.__name__, obj.__class__.__name__))
same_value = False
old_value = self.values.get(obj)
if value is old_value:
return
elif value is not None and value == old_value:
value.__dirty__ = old_value.__dirty__
same_value = True
if old_value is not None:
obj.element.remove(old_value.element)
if value is not None:
obj._insert_element(value.element)
self.values[obj] = value
if not same_value:
obj.__dirty__ = True
if self.onset:
self.onset(obj, self, value)
def __delete__(self, obj):
try:
old_value = self.values.pop(obj)
except KeyError:
pass
else:
if old_value is not None:
obj.element.remove(old_value.element)
obj.__dirty__ = True
if self.ondel:
self.ondel(obj, self)
class XMLStringChoiceChild(XMLElementChoiceChild):
"""
A choice between keyword strings from a registry, custom strings from the
other type and custom extensions. This descriptor will accept and return
strings instead of requiring XMLElement instances for the values in the
registry and the other type. Check XMLEmptyElementRegistryType for a
metaclass for building registries of XMLEmptyElement classes for keywords.
"""
def __init__(self, name, registry=None, other_type=None, extension_type=None):
self.registry = registry
self.other_type = other_type
self.extension_type = extension_type
types = registry.classes if registry is not None else ()
types += (other_type,) if other_type is not None else ()
super(XMLStringChoiceChild, self).__init__(name, types, extension_type=extension_type, required=True, test_equal=True)
def __get__(self, obj, objtype):
value = super(XMLStringChoiceChild, self).__get__(obj, objtype)
if obj is None or value is None or isinstance(value, self.extension_type or ()):
return value
else:
return unicode(value)
def __set__(self, obj, value):
if isinstance(value, basestring):
if self.registry is not None and value in self.registry.names:
value = self.registry.class_map[value]()
elif self.other_type is not None:
value = self.other_type.from_string(value)
super(XMLStringChoiceChild, self).__set__(obj, value)
## XMLElement base classes
class XMLElementBase(object):
"""
This class is used as a common ancestor for XML elements and provides
the means for super() to find at least dummy implementations for the
methods that are supposed to be implemented by subclasses, even when
they are not implemented by any other ancestor class. This is necessary
in order to simplify access to these methods when multiple inheritance
is involved and none or only some of the classes implement them.
The methods declared here should to be implemented in subclasses as
necessary.
"""
def __get_dirty__(self):
return False
def __set_dirty__(self, dirty):
return
def _build_element(self):
return
def _parse_element(self, element):
return
class XMLElementType(type):
def __init__(cls, name, bases, dct):
super(XMLElementType, cls).__init__(name, bases, dct)
# set dictionary of xml attributes and xml child elements
cls._xml_attributes = {}
cls._xml_element_children = {}
cls._xml_children_qname_map = {}
for base in reversed(bases):
if hasattr(base, '_xml_attributes'):
cls._xml_attributes.update(base._xml_attributes)
if hasattr(base, '_xml_element_children') and hasattr(base, '_xml_children_qname_map'):
cls._xml_element_children.update(base._xml_element_children)
cls._xml_children_qname_map.update(base._xml_children_qname_map)
for name, value in dct.iteritems():
if isinstance(value, XMLElementID):
if cls._xml_id is not None:
raise AttributeError("Only one XMLElementID attribute can be defined in the %s class" % cls.__name__)
cls._xml_id = value
cls._xml_attributes[value.name] = value
elif isinstance(value, XMLAttribute):
cls._xml_attributes[value.name] = value
elif isinstance(value, XMLElementChild):
cls._xml_element_children[value.name] = value
cls._xml_children_qname_map[value.type.qname] = (value, value.type)
elif isinstance(value, XMLElementChoiceChild):
cls._xml_element_children[value.name] = value
for type in value.types:
cls._xml_children_qname_map[type.qname] = (value, type)
# register class in its XMLDocument
if cls._xml_document is not None:
cls._xml_document.register_element(cls)
class XMLElement(XMLElementBase):
__metaclass__ = XMLElementType
_xml_tag = None # To be defined in subclass
_xml_namespace = None # To be defined in subclass
_xml_document = None # To be defined in subclass
_xml_extension_type = None # Can be defined in subclass
_xml_id = None # Can be defined in subclass, or will be set by the metaclass to the XMLElementID attribute (if present)
_xml_children_order = {} # Can be defined in subclass
# dynamically generated
_xml_attributes = {}
_xml_element_children = {}
_xml_children_qname_map = {}
qname = classproperty(lambda cls: '{%s}%s' % (cls._xml_namespace, cls._xml_tag))
def __init__(self):
self.element = etree.Element(self.qname, nsmap=self._xml_document.nsmap)
self.__dirty__ = True
def __get_dirty__(self):
return (self.__dict__['__dirty__']
or any(child.__dirty__ for child in (getattr(self, name) for name in self._xml_element_children) if child is not None)
or super(XMLElement, self).__get_dirty__())
def __set_dirty__(self, dirty):
super(XMLElement, self).__set_dirty__(dirty)
if not dirty:
for child in (child for child in (getattr(self, name) for name in self._xml_element_children) if child is not None):
child.__dirty__ = dirty
self.__dict__['__dirty__'] = dirty
__dirty__ = property(__get_dirty__, __set_dirty__)
def check_validity(self):
# check attributes
for name, attribute in self._xml_attributes.iteritems():
# if attribute has default but it was not set, will also be added with this occasion
value = getattr(self, name, None)
if attribute.required and value is None:
raise ValidationError("required attribute %s of %s is not set" % (name, self.__class__.__name__))
# check element children
for name, element_child in self._xml_element_children.iteritems():
# if child has default but it was not set, will also be added with this occasion
child = getattr(self, name, None)
if child is None and element_child.required:
raise ValidationError("element child %s of %s is not set" % (name, self.__class__.__name__))
def to_element(self):
try:
self.check_validity()
except ValidationError, e:
raise BuilderError(str(e))
# build element children
for name in self._xml_element_children:
child = getattr(self, name, None)
if child is not None:
child.to_element()
self._build_element()
return self.element
@classmethod
def from_element(cls, element, xml_document=None):
obj = cls.__new__(cls)
obj._xml_document = xml_document if xml_document is not None else cls._xml_document
obj.element = element
# set known attributes
for name, attribute in cls._xml_attributes.iteritems():
xmlvalue = element.get(attribute.xmlname, None)
if xmlvalue is not None:
try:
setattr(obj, name, attribute.parse(xmlvalue))
except (ValueError, TypeError):
raise ValidationError("got illegal value for attribute %s of %s: %s" % (name, cls.__name__, xmlvalue))
# set element children
for child in element:
element_child, type = cls._xml_children_qname_map.get(child.tag, (None, None))
if element_child is not None:
try:
value = type.from_element(child, xml_document=obj._xml_document)
except ValidationError:
pass # we should accept partially valid documents
else:
setattr(obj, element_child.name, value)
obj._parse_element(element)
obj.check_validity()
obj.__dirty__ = False
return obj
@classmethod
def _register_xml_attribute(cls, attribute, element):
cls._xml_element_children[attribute] = element
cls._xml_children_qname_map[element.type.qname] = (element, element.type)
for subclass in cls.__subclasses__():
subclass._register_xml_attribute(attribute, element)
@classmethod
def _unregister_xml_attribute(cls, attribute):
element = cls._xml_element_children.pop(attribute)
del cls._xml_children_qname_map[element.type.qname]
for subclass in cls.__subclasses__():
subclass._unregister_xml_attribute(attribute)
@classmethod
def register_extension(cls, attribute, type, test_equal=True):
if cls._xml_extension_type is None:
raise ValueError("XMLElement type %s does not support extensions (requested extension type %s)" % (cls.__name__, type.__name__))
elif not issubclass(type, cls._xml_extension_type):
raise TypeError("XMLElement type %s only supports extensions of type %s (requested extension type %s)" % (cls.__name__, cls._xml_extension_type, type.__name__))
elif hasattr(cls, attribute):
raise ValueError("XMLElement type %s already has an attribute named %s (requested extension type %s)" % (cls.__name__, attribute, type.__name__))
extension = XMLElementChild(attribute, type=type, required=False, test_equal=test_equal)
setattr(cls, attribute, extension)
cls._register_xml_attribute(attribute, extension)
@classmethod
def unregister_extension(cls, attribute):
if cls._xml_extension_type is None:
raise ValueError("XMLElement type %s does not support extensions" % cls.__name__)
cls._unregister_xml_attribute(attribute)
delattr(cls, attribute)
def _insert_element(self, element):
if element in self.element:
return
order = self._xml_children_order.get(element.tag, self._xml_children_order.get(None, sys.maxint))
for i in xrange(len(self.element)):
child_order = self._xml_children_order.get(self.element[i].tag, self._xml_children_order.get(None, sys.maxint))
if child_order > order:
position = i
break
else:
position = len(self.element)
self.element.insert(position, element)
def __eq__(self, other):
if isinstance(other, XMLElement):
if self is other:
return True
for name, attribute in self._xml_attributes.iteritems():
if attribute.test_equal:
if not hasattr(other, name) or getattr(self, name) != getattr(other, name):
return False
for name, element_child in self._xml_element_children.iteritems():
if element_child.test_equal:
if not hasattr(other, name) or getattr(self, name) != getattr(other, name):
return False
try:
__eq__ = super(XMLElement, self).__eq__
except AttributeError:
return True
else:
return __eq__(other)
elif self.__class__._xml_id is not None:
return self._xml_id == other
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def __hash__(self):
if self.__class__._xml_id is not None:
return hash(self._xml_id)
else:
return object.__hash__(self)
class XMLRootElementType(XMLElementType):
def __init__(cls, name, bases, dct):
super(XMLRootElementType, cls).__init__(name, bases, dct)
if cls._xml_document is not None:
if cls._xml_document.root_element is not None:
raise TypeError('there is already a root element registered for %s' % cls.__name__)
cls._xml_document.root_element = cls
class XMLRootElement(XMLElement):
__metaclass__ = XMLRootElementType
def __init__(self):
XMLElement.__init__(self)
self.__cache__ = WeakValueDictionary({self.element: self})
@classmethod
def from_element(cls, element, xml_document=None):
obj = super(XMLRootElement, cls).from_element(element, xml_document)
obj.__cache__ = WeakValueDictionary({obj.element: obj})
return obj
@classmethod
def parse(cls, document):
return cls._xml_document.parse(document)
def toxml(self, encoding=None, pretty_print=False, validate=True):
return self._xml_document.build(self, encoding=encoding, pretty_print=pretty_print, validate=validate)
def xpath(self, xpath, namespaces=None):
result = []
try:
nodes = self.element.xpath(xpath, namespaces=namespaces)
except etree.XPathError:
raise ValueError("illegal XPath expression")
for element in (node for node in nodes if isinstance(node, etree._Element)):
if element in self.__cache__:
result.append(self.__cache__[element])
continue
if element is self.element:
self.__cache__[element] = self
result.append(self)
continue
for ancestor in element.iterancestors():
if ancestor in self.__cache__:
container = self.__cache__[ancestor]
break
else:
container = self
notvisited = deque([container])
visited = set()
while notvisited:
container = notvisited.popleft()
self.__cache__[container.element] = container
if isinstance(container, XMLListMixin):
children = set(child for child in container if isinstance(child, XMLElement) and child not in visited)
visited.update(children)
notvisited.extend(children)
for child in container._xml_element_children:
value = getattr(container, child)
if value is not None and value not in visited:
visited.add(value)
notvisited.append(value)
if element in self.__cache__:
result.append(self.__cache__[element])
return result
def get_xpath(self, element):
raise NotImplementedError
def find_parent(self, element):
raise NotImplementedError
## Mixin classes
class ThisClass(object):
"""
Special marker class that is used to indicate that an XMLListElement
subclass can be an item of itself. This is necessary because a class
cannot reference itself when defining _xml_item_type
"""
class XMLListMixinType(type):
def __init__(cls, name, bases, dct):
super(XMLListMixinType, cls).__init__(name, bases, dct)
if '_xml_item_type' in dct:
cls._xml_item_type = cls._xml_item_type # trigger __setattr__
def __setattr__(cls, name, value):
if name == '_xml_item_type':
if value is ThisClass:
value = cls
elif isinstance(value, tuple) and ThisClass in value:
value = tuple(cls if type is ThisClass else type for type in value)
if value is None:
cls._xml_item_element_types = ()
cls._xml_item_extension_types = ()
else:
item_types = value if isinstance(value, tuple) else (value,)
cls._xml_item_element_types = tuple(type for type in item_types if issubclass(type, XMLElement))
cls._xml_item_extension_types = tuple(type for type in item_types if not issubclass(type, XMLElement))
super(XMLListMixinType, cls).__setattr__(name, value)
class XMLListMixin(XMLElementBase):
"""A mixin representing a list of other XML elements"""
__metaclass__ = XMLListMixinType
_xml_item_type = None
def __new__(cls, *args, **kw):
if cls._xml_item_type is None:
raise TypeError("The %s class cannot be instantiated because it doesn't define the _xml_item_type attribute" % cls.__name__)
instance = super(XMLListMixin, cls).__new__(cls)
instance._element_map = {}
instance._xmlid_map = defaultdict(dict)
return instance
def __contains__(self, item):
return item in self._element_map.itervalues()
def __iter__(self):
return (self._element_map[element] for element in self.element if element in self._element_map)
def __len__(self):
return len(self._element_map)
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, list(self))
def __eq__(self, other):
if isinstance(other, XMLListMixin):
return self is other or (len(self) == len(other) and all(self_item == other_item for self_item, other_item in izip(self, other)))
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def __getitem__(self, key):
if key is IterateTypes:
return (cls for cls, mapping in self._xmlid_map.iteritems() if mapping)
if not isinstance(key, tuple):
raise KeyError(key)
try:
cls, id = key
except ValueError:
raise KeyError(key)
if id is IterateIDs:
return self._xmlid_map[cls].iterkeys()
elif id is IterateItems:
return self._xmlid_map[cls].itervalues()
else:
return self._xmlid_map[cls][id]
def __delitem__(self, key):
if not isinstance(key, tuple):
raise KeyError(key)
try:
cls, id = key
except ValueError:
raise KeyError(key)
if id is All:
for item in self._xmlid_map[cls].values():
self.remove(item)
else:
self.remove(self._xmlid_map[cls][id])
def __get_dirty__(self):
return any(item.__dirty__ for item in self._element_map.itervalues()) or super(XMLListMixin, self).__get_dirty__()
def __set_dirty__(self, dirty):
super(XMLListMixin, self).__set_dirty__(dirty)
if not dirty:
for item in self._element_map.itervalues():
item.__dirty__ = dirty
def _parse_element(self, element):
super(XMLListMixin, self)._parse_element(element)
self._element_map.clear()
self._xmlid_map.clear()
for child in element[:]:
child_class = self._xml_document.get_element(child.tag, type(None))
if child_class in self._xml_item_element_types or issubclass(child_class, self._xml_item_extension_types):
try:
value = child_class.from_element(child, xml_document=self._xml_document)
except ValidationError:
pass
else:
if value._xml_id is not None and value._xml_id in self._xmlid_map[child_class]:
element.remove(child)
else:
if value._xml_id is not None:
self._xmlid_map[child_class][value._xml_id] = value
self._element_map[value.element] = value
def _build_element(self):
super(XMLListMixin, self)._build_element()
for child in self._element_map.itervalues():
child.to_element()
def add(self, item):
if not (item.__class__ in self._xml_item_element_types or isinstance(item, self._xml_item_extension_types)):
raise TypeError("%s cannot add items of type %s" % (self.__class__.__name__, item.__class__.__name__))
same_value = False
if item._xml_id is not None and item._xml_id in self._xmlid_map[item.__class__]:
old_item = self._xmlid_map[item.__class__][item._xml_id]
if item is old_item:
return
elif item == old_item:
item.__dirty__ = old_item.__dirty__
same_value = True
self.element.remove(old_item.element)
del self._xmlid_map[item.__class__][item._xml_id]
del self._element_map[old_item.element]
self._insert_element(item.element)
if item._xml_id is not None:
self._xmlid_map[item.__class__][item._xml_id] = item
self._element_map[item.element] = item
if not same_value:
self.__dirty__ = True
def remove(self, item):
self.element.remove(item.element)
if item._xml_id is not None:
del self._xmlid_map[item.__class__][item._xml_id]
del self._element_map[item.element]
self.__dirty__ = True
def update(self, sequence):
for item in sequence:
self.add(item)
def clear(self):
for item in self._element_map.values():
self.remove(item)
## Element types
class XMLSimpleElement(XMLElement):
_xml_value_type = None # To be defined in subclass
def __new__(cls, *args, **kw):
if cls._xml_value_type is None:
raise TypeError("The %s class cannot be instantiated because it doesn't define the _xml_value_type attribute" % cls.__name__)
return super(XMLSimpleElement, cls).__new__(cls)
def __init__(self, value):
XMLElement.__init__(self)
self.value = value
def __eq__(self, other):
if isinstance(other, XMLSimpleElement):
return self is other or self.value == other.value
else:
return self.value == other
def __nonzero__(self):
return bool(self.value)
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.value)
def __str__(self):
return str(self.value)
def __unicode__(self):
return unicode(self.value)
def _get_value(self):
return self.__dict__['value']
def _set_value(self, value):
if not isinstance(value, self._xml_value_type):
value = self._xml_value_type(value)
if self.__dict__.get('value', Null) == value:
return
self.__dict__['value'] = value
self.__dirty__ = True
value = property(_get_value, _set_value)
del _get_value, _set_value
def _parse_element(self, element):
super(XMLSimpleElement, self)._parse_element(element)
value = element.text or u''
if hasattr(self._xml_value_type, '__xmlparse__'):
self.value = self._xml_value_type.__xmlparse__(value)
else:
self.value = self._xml_value_type(value)
def _build_element(self):
super(XMLSimpleElement, self)._build_element()
if hasattr(self.value, '__xmlbuild__'):
self.element.text = self.value.__xmlbuild__()
else:
self.element.text = unicode(self.value)
class XMLStringElement(XMLSimpleElement):
_xml_value_type = unicode # Can be overwritten in subclasses
def __len__(self):
return len(self.value)
class XMLLocalizedStringElement(XMLStringElement):
lang = XMLAttribute('lang', xmlname='{http://www.w3.org/XML/1998/namespace}lang', type=str, required=False, test_equal=True)
def __init__(self, value, lang=None):
XMLStringElement.__init__(self, value)
self.lang = lang
def __eq__(self, other):
if isinstance(other, XMLLocalizedStringElement):
return self is other or (self.lang == other.lang and self.value == other.value)
elif self.lang is None:
return XMLStringElement.__eq__(self, other)
else:
return NotImplemented
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.value, self.lang)
def _parse_element(self, element):
super(XMLLocalizedStringElement, self)._parse_element(element)
self.lang = element.get('{http://www.w3.org/XML/1998/namespace}lang', None)
class XMLBooleanElement(XMLSimpleElement):
_xml_value_type = Boolean
class XMLByteElement(XMLSimpleElement):
_xml_value_type = Byte
class XMLUnsignedByteElement(XMLSimpleElement):
_xml_value_type = UnsignedByte
class XMLShortElement(XMLSimpleElement):
_xml_value_type = Short
class XMLUnsignedShortElement(XMLSimpleElement):
_xml_value_type = UnsignedShort
class XMLIntElement(XMLSimpleElement):
_xml_value_type = Int
class XMLUnsignedIntElement(XMLSimpleElement):
_xml_value_type = UnsignedInt
class XMLLongElement(XMLSimpleElement):
_xml_value_type = Long
class XMLUnsignedLongElement(XMLSimpleElement):
_xml_value_type = UnsignedLong
class XMLIntegerElement(XMLSimpleElement):
_xml_value_type = int
class XMLPositiveIntegerElement(XMLSimpleElement):
_xml_value_type = PositiveInteger
class XMLNegativeIntegerElement(XMLSimpleElement):
_xml_value_type = NegativeInteger
class XMLNonNegativeIntegerElement(XMLSimpleElement):
_xml_value_type = NonNegativeInteger
class XMLNonPositiveIntegerElement(XMLSimpleElement):
_xml_value_type = NonPositiveInteger
class XMLDecimalElement(XMLSimpleElement):
_xml_value_type = Decimal
class XMLDateTimeElement(XMLSimpleElement):
_xml_value_type = DateTime
class XMLAnyURIElement(XMLStringElement):
_xml_value_type = AnyURI
class XMLEmptyElement(XMLElement):
def __repr__(self):
return '%s()' % self.__class__.__name__
def __eq__(self, other):
return type(self) is type(other) or NotImplemented
def __hash__(self):
return hash(self.__class__)
class XMLEmptyElementRegistryType(type):
"""A metaclass for building registries of XMLEmptyElement subclasses from names"""
def __init__(cls, name, bases, dct):
super(XMLEmptyElementRegistryType, cls).__init__(name, bases, dct)
typename = getattr(cls, '__typename__', name.partition('Registry')[0]).capitalize()
class BaseElementType(XMLEmptyElement):
def __str__(self): return self._xml_tag
def __unicode__(self): return unicode(self._xml_tag)
cls.__basetype__ = BaseElementType
cls.__basetype__.__name__ = 'Base%sType' % typename
cls.class_map = {}
for name in cls.names:
class ElementType(BaseElementType):
_xml_tag = name
_xml_namespace = cls._xml_namespace
_xml_document = cls._xml_document
_xml_id = name
ElementType.__name__ = typename + name.title().translate(None, '-_')
cls.class_map[name] = ElementType
cls.classes = tuple(cls.class_map[name] for name in cls.names)
## Created using mixins
class XMLListElementType(XMLElementType, XMLListMixinType): pass
class XMLListRootElementType(XMLRootElementType, XMLListMixinType): pass
class XMLListElement(XMLElement, XMLListMixin):
__metaclass__ = XMLListElementType
class XMLListRootElement(XMLRootElement, XMLListMixin):
__metaclass__ = XMLListRootElementType
class XMLStringListElementType(XMLListElementType):
def __init__(cls, name, bases, dct):
if cls._xml_item_type is not None:
raise TypeError("The %s class should not define _xml_item_type, but define _xml_item_registry, _xml_item_other_type and _xml_item_extension_type instead" % cls.__name__)
types = cls._xml_item_registry.classes if cls._xml_item_registry is not None else ()
types += tuple(type for type in (cls._xml_item_other_type, cls._xml_item_extension_type) if type is not None)
cls._xml_item_type = types or None
super(XMLStringListElementType, cls).__init__(name, bases, dct)
class XMLStringListElement(XMLListElement):
__metaclass__ = XMLStringListElementType
_xml_item_registry = None
_xml_item_other_type = None
_xml_item_extension_type = None
def __contains__(self, item):
if isinstance(item, basestring):
if self._xml_item_registry is not None and item in self._xml_item_registry.names:
item = self._xml_item_registry.class_map[item]()
elif self._xml_item_other_type is not None:
item = self._xml_item_other_type.from_string(item)
return item in self._element_map.itervalues()
def __iter__(self):
return (item if isinstance(item, self._xml_item_extension_types) else unicode(item) for item in super(XMLStringListElement, self).__iter__())
def add(self, item):
if isinstance(item, basestring):
if self._xml_item_registry is not None and item in self._xml_item_registry.names:
item = self._xml_item_registry.class_map[item]()
elif self._xml_item_other_type is not None:
item = self._xml_item_other_type.from_string(item)
super(XMLStringListElement, self).add(item)
def remove(self, item):
if isinstance(item, basestring):
if self._xml_item_registry is not None and item in self._xml_item_registry.names:
xmlitem = self._xml_item_registry.class_map[item]()
try:
item = (entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem).next()
except StopIteration:
raise KeyError(item)
elif self._xml_item_other_type is not None:
xmlitem = self._xml_item_other_type.from_string(item)
try:
item = (entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem).next()
except StopIteration:
raise KeyError(item)
super(XMLStringListElement, self).remove(item)
diff --git a/sipsimple/payloads/pidf.py b/sipsimple/payloads/pidf.py
index 6cad582f..209e9255 100644
--- a/sipsimple/payloads/pidf.py
+++ b/sipsimple/payloads/pidf.py
@@ -1,497 +1,498 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""PIDF handling according to RFC3863 and RFC4479"""
__all__ = ['pidf_namespace',
'dm_namespace',
'PIDFDocument',
'ServiceExtension',
'DeviceExtension',
'PersonExtension',
'StatusExtension',
'Note',
'DeviceID',
'Status',
'Basic',
'Contact',
'ServiceTimestamp',
'Service',
'DeviceTimestamp',
'Device',
'PersonTimestamp',
'Person',
'PIDF',
# Extensions
'ExtendedStatus',
'DeviceInfo']
from itertools import izip
+from application.python.weakref import weakobjectmap
+
from sipsimple.payloads import ValidationError, XMLDocument, XMLListRootElement, XMLListElement, XMLElement, XMLAttribute, XMLElementID, XMLElementChild
from sipsimple.payloads import XMLStringElement, XMLLocalizedStringElement, XMLDateTimeElement, XMLAnyURIElement
from sipsimple.payloads.datatypes import AnyURI, ID
-from sipsimple.util import weakobjectmap
pidf_namespace = 'urn:ietf:params:xml:ns:pidf'
dm_namespace = 'urn:ietf:params:xml:ns:pidf:data-model'
class PIDFDocument(XMLDocument):
content_type = 'application/pidf+xml'
PIDFDocument.register_namespace(pidf_namespace, prefix=None, schema='pidf.xsd')
PIDFDocument.register_namespace(dm_namespace, prefix='dm', schema='data-model.xsd')
## Marker mixin
class ServiceExtension(object): pass
class ServiceItemExtension(object): pass
class DeviceExtension(object): pass
class PersonExtension(object): pass
class StatusExtension(object): pass
## Attribute value types
class BasicStatusValue(str):
def __new__(cls, value):
if value not in ('closed', 'open'):
raise ValueError('illegal BasicStatusValue')
return str.__new__(cls, value)
## General elements
class Note(unicode):
def __new__(cls, value, lang=None):
instance = unicode.__new__(cls, value)
instance.lang = lang
return instance
def __repr__(self):
return "%s(%s, lang=%r)" % (self.__class__.__name__, unicode.__repr__(self), self.lang)
def __eq__(self, other):
if isinstance(other, Note):
return unicode.__eq__(self, other) and self.lang == other.lang
elif isinstance(other, basestring):
return self.lang is None and unicode.__eq__(self, other)
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class PIDFNote(XMLLocalizedStringElement):
_xml_tag = 'note'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
def __unicode__(self):
return Note(self.value, self.lang)
class DMNote(XMLLocalizedStringElement):
_xml_tag = 'note'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
def __unicode__(self):
return Note(self.value, self.lang)
class NoteMap(object):
"""Descriptor to be used for _note_map attributes on XML elements with notes"""
def __init__(self):
self.object_map = weakobjectmap()
def __get__(self, obj, type):
if obj is None:
return self
try:
return self.object_map[obj]
except KeyError:
return self.object_map.setdefault(obj, {})
def __set__(self, obj, value):
raise AttributeError("cannot set attribute")
def __delete__(self, obj):
raise AttributeError("cannot delete attribute")
class NoteList(object):
def __init__(self, xml_element, note_type):
self.xml_element = xml_element
self.note_type = note_type
def __contains__(self, item):
if isinstance(item, Note):
item = self.note_type(item, item.lang)
elif isinstance(item, basestring):
item = self.note_type(item)
return item in self.xml_element._note_map.itervalues()
def __iter__(self):
return (unicode(self.xml_element._note_map[element]) for element in self.xml_element.element if element in self.xml_element._note_map)
def __len__(self):
return len(self.xml_element._note_map)
def __eq__(self, other):
if isinstance(other, NoteList):
return self is other or (len(self) == len(other) and all(self_item == other_item for self_item, other_item in izip(self, other)))
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def _parse_element(self, element):
self.xml_element._note_map.clear()
for child in element:
if child.tag == self.note_type.qname:
try:
note = self.note_type.from_element(child, xml_document=self.xml_element._xml_document)
except ValidationError:
pass
else:
self.xml_element._note_map[note.element] = note
def _build_element(self):
for note in self.xml_element._note_map.itervalues():
note.to_element()
def add(self, item):
if isinstance(item, Note):
item = self.note_type(item, item.lang)
elif isinstance(item, basestring):
item = self.note_type(item)
if type(item) is not self.note_type:
raise TypeError("%s cannot add notes of type %s" % (self.xml_element.__class__.__name__, item.__class__.__name__))
self.xml_element._insert_element(item.element)
self.xml_element._note_map[item.element] = item
self.xml_element.__dirty__ = True
def remove(self, item):
if isinstance(item, Note):
try:
item = (entry for entry in self.xml_element._note_map.itervalues() if unicode(entry) == item).next()
except StopIteration:
raise KeyError(item)
elif isinstance(item, basestring):
try:
item = (entry for entry in self.xml_element._note_map.itervalues() if entry == item).next()
except StopIteration:
raise KeyError(item)
if type(item) is not self.note_type:
raise KeyError(item)
self.xml_element.element.remove(item.element)
del self.xml_element._note_map[item.element]
self.xml_element.__dirty__ = True
def update(self, sequence):
for item in sequence:
self.add(item)
def clear(self):
for item in self.xml_element._note_map.values():
self.remove(item)
class DeviceID(XMLStringElement):
_xml_tag = 'deviceID'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
## Service elements
class Basic(XMLStringElement):
_xml_tag = 'basic'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_value_type = BasicStatusValue
class Status(XMLElement):
_xml_tag = 'status'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_extension_type = StatusExtension
_xml_children_order = {Basic.qname: 0}
basic = XMLElementChild('basic', type=Basic, required=False, test_equal=True)
def __init__(self, basic=None):
XMLElement.__init__(self)
self.basic = basic
def check_validity(self):
if len(self.element) == 0:
raise ValidationError("Status objects must have at least one child")
super(Status, self).check_validity()
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.basic)
class Contact(XMLAnyURIElement):
_xml_tag = 'contact'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
priority = XMLAttribute('priority', type=float, required=False, test_equal=False)
class ServiceTimestamp(XMLDateTimeElement):
_xml_tag = 'timestamp'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
class Service(XMLListElement):
_xml_tag = 'tuple'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_extension_type = ServiceExtension
_xml_item_type = (DeviceID, ServiceItemExtension)
_xml_children_order = {Status.qname: 0,
None: 1,
Contact.qname: 2,
PIDFNote.qname: 3,
ServiceTimestamp.qname: 4}
id = XMLElementID('id', type=ID, required=True, test_equal=True)
status = XMLElementChild('status', type=Status, required=True, test_equal=True)
contact = XMLElementChild('contact', type=Contact, required=False, test_equal=True)
timestamp = XMLElementChild('timestamp', type=ServiceTimestamp, required=False, test_equal=True)
_note_map = NoteMap()
def __init__(self, id, notes=[], status=None, contact=None, timestamp=None):
XMLListElement.__init__(self)
self.id = id
self.status = status
self.contact = contact
self.timestamp = timestamp
self.notes.update(notes)
@property
def notes(self):
return NoteList(self, PIDFNote)
def __eq__(self, other):
if isinstance(other, Service):
return super(Service, self).__eq__(other) and self.notes == other.notes
else:
return self.id == other
def __repr__(self):
return '%s(%r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, list(self.notes), self.status, self.contact, self.timestamp)
def _parse_element(self, element):
super(Service, self)._parse_element(element)
self.notes._parse_element(element)
def _build_element(self):
super(Service, self)._build_element()
self.notes._build_element()
class DeviceTimestamp(XMLDateTimeElement):
_xml_tag = 'timestamp'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
class Device(XMLElement):
_xml_tag = 'device'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
_xml_extension_type = DeviceExtension
_xml_children_order = {None: 0,
DeviceID.qname: 1,
DMNote.qname: 2,
DeviceTimestamp.qname: 3}
id = XMLElementID('id', type=ID, required=True, test_equal=True)
device_id = XMLElementChild('device_id', type=DeviceID, required=False, test_equal=True)
timestamp = XMLElementChild('timestamp', type=DeviceTimestamp, required=False, test_equal=True)
_note_map = NoteMap()
def __init__(self, id, device_id=None, notes=[], timestamp=None):
XMLElement.__init__(self)
self.id = id
self.device_id = device_id
self.timestamp = timestamp
self.notes.update(notes)
@property
def notes(self):
return NoteList(self, DMNote)
def __eq__(self, other):
if isinstance(other, Device):
return super(Device, self).__eq__(other) and self.notes == other.notes
else:
return self.id == other
def __repr__(self):
return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.device_id, list(self.notes), self.timestamp)
def _parse_element(self, element):
super(Device, self)._parse_element(element)
self.notes._parse_element(element)
def _build_element(self):
super(Device, self)._build_element()
self.notes._build_element()
class PersonTimestamp(XMLDateTimeElement):
_xml_tag = 'timestamp'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
class Person(XMLElement):
_xml_tag = 'person'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
_xml_extension_type = PersonExtension
_xml_children_order = {None: 0,
DMNote.qname: 1,
PersonTimestamp.qname: 2}
id = XMLElementID('id', type=ID, required=True, test_equal=True)
timestamp = XMLElementChild('timestamp', type=PersonTimestamp, required=False, test_equal=True)
_note_map = NoteMap()
def __init__(self, id, notes=[], timestamp=None):
XMLElement.__init__(self)
self.id = id
self.timestamp = timestamp
self.notes.update(notes)
@property
def notes(self):
return NoteList(self, DMNote)
def __eq__(self, other):
if isinstance(other, Person):
return super(Person, self).__eq__(other) and self.notes == other.notes
else:
return self.id == other
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.id, list(self.notes), self.timestamp)
def _parse_element(self, element):
super(Person, self)._parse_element(element)
self.notes._parse_element(element)
def _build_element(self):
super(Person, self)._build_element()
self.notes._build_element()
class PIDF(XMLListRootElement):
_xml_tag = 'presence'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_children_order = {Service.qname: 0,
PIDFNote.qname: 1,
Person.qname: 2,
Device.qname: 3}
_xml_item_type = (Service, PIDFNote, Person, Device)
entity = XMLAttribute('entity', type=AnyURI, required=True, test_equal=True)
services = property(lambda self: (item for item in self if type(item) is Service))
notes = property(lambda self: (item for item in self if type(item) is Note))
persons = property(lambda self: (item for item in self if type(item) is Person))
devices = property(lambda self: (item for item in self if type(item) is Device))
def __init__(self, entity, elements=[]):
XMLListRootElement.__init__(self)
self.entity = entity
self.update(elements)
def __contains__(self, item):
if isinstance(item, Note):
item = PIDFNote(item, item.lang)
return super(PIDF, self).__contains__(item)
def __iter__(self):
return (unicode(item) if type(item) is PIDFNote else item for item in super(PIDF, self).__iter__())
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.entity, list(self))
def add(self, item):
if isinstance(item, Note):
item = PIDFNote(item, item.lang)
super(PIDF, self).add(item)
def remove(self, item):
if isinstance(item, Note):
try:
item = (entry for entry in super(PIDF, self).__iter__() if type(entry) is PIDFNote and unicode(entry) == item).next()
except StopIteration:
raise KeyError(item)
super(PIDF, self).remove(item)
#
# Extensions
#
agp_pidf_namespace = 'urn:ag-projects:xml:ns:pidf'
PIDFDocument.register_namespace(agp_pidf_namespace, prefix='agp-pidf')
class ExtendedStatusValue(str):
def __new__(cls, value):
if value not in ('available', 'offline', 'away', 'extended-away', 'busy'):
raise ValueError("illegal value for extended status")
return str.__new__(cls, value)
class ExtendedStatus(XMLStringElement, StatusExtension):
_xml_tag = 'extended'
_xml_namespace = agp_pidf_namespace
_xml_document = PIDFDocument
_xml_value_type = ExtendedStatusValue
Status.register_extension('extended', type=ExtendedStatus)
class DeviceInfo(XMLElement, ServiceExtension):
_xml_tag = 'device-info'
_xml_namespace = agp_pidf_namespace
_xml_document = PIDFDocument
id = XMLElementID('id', type=str, required=True, test_equal=True)
description = XMLAttribute('description', type=str, required=False, test_equal=True)
def __init__(self, id, description=None):
XMLElement.__init__(self)
self.id = id
self.description = description
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.id, self.description)
Service.register_extension('device_info', type=DeviceInfo)
diff --git a/sipsimple/util.py b/sipsimple/util.py
index 1248083d..fdb5dfeb 100644
--- a/sipsimple/util.py
+++ b/sipsimple/util.py
@@ -1,283 +1,155 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""Implements utilities commonly used in various parts of the library"""
from __future__ import absolute_import
-__all__ = ["MarkerType", "All", "Any", "MultilingualText", "Timestamp", "TimestampedNotificationData", "user_info", "weakobjectmap"]
+__all__ = ["All", "Any", "MultilingualText", "Timestamp", "TimestampedNotificationData", "user_info"]
import os
import platform
import re
import sys
import weakref
from collections import Mapping
from datetime import datetime
from application.notification import NotificationData
-from application.python.types import Singleton
+from application.python.types import Singleton, MarkerType
from dateutil.tz import tzoffset
# Utility classes
#
-class MarkerType(type):
- def __call__(cls, *args, **kw):
- return cls
- def __repr__(cls):
- return cls.__name__
-
-
class All(object):
__metaclass__ = MarkerType
class Any(object):
__metaclass__ = MarkerType
class MultilingualText(unicode):
def __new__(cls, *args, **translations):
if len(args) > 1:
raise TypeError("%s.__new__ takes at most 1 positional argument (%d given)" % (cls.__name__, len(args)))
default = args[0] if args else translations.get('en', u'')
obj = unicode.__new__(cls, default)
obj.translations = translations
return obj
def get_translation(self, language):
return self.translations.get(language, self)
class Timestamp(datetime):
_timestamp_re = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.(?P<secfrac>\d{1,}))?((?P<UTC>Z)|((?P<tzsign>\+|-)(?P<tzhour>\d{2}):(?P<tzminute>\d{2})))')
@classmethod
def utc_offset(cls):
timediff = datetime.now() - datetime.utcnow()
return int(round((timediff.days*86400 + timediff.seconds + timediff.microseconds/1000000.0)/60))
@classmethod
def parse(cls, stamp):
if stamp is None:
return None
match = cls._timestamp_re.match(stamp)
if match is None:
raise ValueError("Timestamp %s is not in RFC3339 format" % stamp)
dct = match.groupdict()
if dct['UTC'] is not None:
secoffset = 0
else:
secoffset = int(dct['tzminute'])*60 + int(dct['tzhour'])*3600
if dct['tzsign'] == '-':
secoffset *= -1
tzinfo = tzoffset(None, secoffset)
if dct['secfrac'] is not None:
secfrac = dct['secfrac'][:6]
secfrac += '0'*(6-len(secfrac))
secfrac = int(secfrac)
else:
secfrac = 0
dt = datetime(int(dct['year']), month=int(dct['month']), day=int(dct['day']),
hour=int(dct['hour']), minute=int(dct['minute']), second=int(dct['second']),
microsecond=secfrac, tzinfo=tzinfo)
return cls(dt)
@classmethod
def format(cls, dt):
if dt is None:
return None
if dt.tzinfo is not None:
return dt.replace(microsecond=0).isoformat()
minutes = cls.utc_offset()
if minutes == 0:
tzspec = 'Z'
else:
if minutes < 0:
sign = '-'
minutes *= -1
else:
sign = '+'
hours = minutes / 60
minutes = minutes % 60
tzspec = '%s%02d:%02d' % (sign, hours, minutes)
return dt.replace(microsecond=0).isoformat()+tzspec
def __new__(cls, value, *args, **kwargs):
if isinstance(value, cls):
return value
elif isinstance(value, datetime):
return cls(value.year, month=value.month, day=value.day,
hour=value.hour, minute=value.minute, second=value.second,
microsecond=value.microsecond, tzinfo=value.tzinfo)
elif isinstance(value, basestring):
return cls.parse(value)
else:
return datetime.__new__(cls, value, *args, **kwargs)
def __str__(self):
return self.format(self)
class TimestampedNotificationData(NotificationData):
def __init__(self, **kwargs):
self.timestamp = datetime.now()
NotificationData.__init__(self, **kwargs)
-class objectref(weakref.ref):
- __slots__ = ("id",)
- def __init__(self, object, discard_callback):
- super(objectref, self).__init__(object, discard_callback)
- self.id = id(object)
-
-
-class weakobjectid(long):
- def __new__(cls, object, discard_callback):
- instance = long.__new__(cls, id(object))
- instance.ref = objectref(object, discard_callback)
- return instance
-
-
-class objectid(long):
- def __new__(cls, object):
- instance = long.__new__(cls, id(object))
- instance.object = object
- return instance
-
-
-class weakobjectmap(dict):
- def __init__(self, *args, **kw):
- def remove(wr, selfref=weakref.ref(self)):
- self = selfref()
- if self is not None:
- super(weakobjectmap, self).__delitem__(wr.id)
- self.__remove__ = remove
- weakobjectmap.update(self, *args, **kw)
-
- def __getitem__(self, key):
- try:
- return super(weakobjectmap, self).__getitem__(objectid(key))
- except KeyError:
- raise KeyError(key)
-
- def __setitem__(self, key, value):
- super(weakobjectmap, self).__setitem__(weakobjectid(key, self.__remove__), value)
-
- def __delitem__(self, key):
- try:
- super(weakobjectmap, self).__delitem__(id(key))
- except KeyError:
- raise KeyError(key)
-
- def __contains__(self, key):
- return super(weakobjectmap, self).__contains__(id(key))
-
- def __iter__(self):
- return self.iterkeys()
-
- def __copy__(self):
- return self.__class__(self)
-
- def __deepcopy__(self, memo):
- from copy import deepcopy
- return self.__class__((key, deepcopy(value, memo)) for key, value in self.iteritems())
-
- def __repr__(self):
- return "%s({%s})" % (self.__class__.__name__, ', '.join(('%r: %r' % (key, value) for key, value in self.iteritems())))
-
- def copy(self):
- return self.__copy__()
-
- def iterkeys(self):
- return (key for key in (key.ref() for key in super(weakobjectmap, self).keys()) if key is not None)
-
- def itervalues(self):
- return (value for key, value in ((key.ref(), value) for key, value in super(weakobjectmap, self).items()) if key is not None)
-
- def iteritems(self):
- return ((key, value) for key, value in ((key.ref(), value) for key, value in super(weakobjectmap, self).items()) if key is not None)
-
- def keys(self):
- return [key for key in (key.ref() for key in super(weakobjectmap, self).keys()) if key is not None]
-
- def values(self):
- return [value for key, value in ((key.ref(), value) for key, value in super(weakobjectmap, self).items()) if key is not None]
-
- def items(self):
- return [(key, value) for key, value in ((key.ref(), value) for key, value in super(weakobjectmap, self).items()) if key is not None]
-
- def has_key(self, key):
- return key in self
-
- def get(self, key, default=None):
- return super(weakobjectmap, self).get(id(key), default)
-
- def setdefault(self, key, default=None):
- return super(weakobjectmap, self).setdefault(weakobjectid(key, self.__remove__), default)
-
- def pop(self, key, *args):
- try:
- return super(weakobjectmap, self).pop(id(key), *args)
- except KeyError:
- raise KeyError(key)
-
- def popitem(self):
- while True:
- key, value = super(weakobjectmap, self).popitem()
- object = key.ref()
- if object is not None:
- return object, value
-
- def update(self, *args, **kw):
- if len(args) > 1:
- raise TypeError("expected at most 1 positional argument (got %d)" % len(args))
- other = args[0] if args else ()
- if isinstance(other, Mapping):
- for key, value in other.iteritems():
- self[key] = value
- elif hasattr(other, "keys"):
- for key in other.keys():
- self[key] = other[key]
- else:
- for key, value in other:
- self[key] = value
- for key, value in kw.iteritems():
- self[key] = value
-
-
# Utility objects
#
class UserInfo(object):
__metaclass__ = Singleton
def __repr__(self):
attribs = ', '.join('%s=%r' % (attr, getattr(self, attr)) for attr in ('username', 'fullname'))
return '%s(%s)' % (self.__class__.__name__, attribs)
@property
def username(self):
if platform.system() == 'Windows':
name = os.getenv('USERNAME')
else:
import pwd
name = pwd.getpwuid(os.getuid()).pw_name
return name.decode(sys.getfilesystemencoding())
@property
def fullname(self):
if platform.system() == 'Windows':
name = os.getenv('USERNAME')
else:
import pwd
name = pwd.getpwuid(os.getuid()).pw_gecos.split(',', 1)[0] or pwd.getpwuid(os.getuid()).pw_name
return name.decode(sys.getfilesystemencoding())
user_info = UserInfo()
del UserInfo

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 3:17 AM (13 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408652
Default Alt Text
(163 KB)

Event Timeline