#!/usr/bin/python2.4 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Defines classes that are needed to model a wavelet.""" import blip import errors import util class DataDocs(object): """Class modeling a bunch of data documents in pythonic way.""" def __init__(self, init_docs, wave_id, wavelet_id, operation_queue): self._docs = init_docs self._wave_id = wave_id self._wavelet_id = wavelet_id self._operation_queue = operation_queue def __iter__(self): return self._docs.__iter__() def __contains__(self, key): return key in self._docs def __delitem__(self, key): if not key in self._docs: return self._operation_queue.wavelet_datadoc_set( self._wave_id, self._wavelet_id, key, None) del self._docs[key] def __getitem__(self, key): return self._docs[key] def __setitem__(self, key, value): self._operation_queue.wavelet_datadoc_set( self._wave_id, self._wavelet_id, key, value) if value is None and key in self._docs: del self._docs[key] else: self._docs[key] = value def __len__(self): return len(self._docs) def keys(self): return self._docs.keys() def serialize(self): """Returns a dictionary of the data documents.""" return self._docs class Participants(object): """Class modelling a set of participants in pythonic way.""" #: Designates full access (read/write) role. ROLE_FULL = "FULL" #: Designates read-only role. ROLE_READ_ONLY = "READ_ONLY" def __init__(self, participants, roles, wave_id, wavelet_id, operation_queue): self._participants = set(participants) self._roles = roles.copy() self._wave_id = wave_id self._wavelet_id = wavelet_id self._operation_queue = operation_queue def __contains__(self, participant): return participant in self._participants def __len__(self): return len(self._participants) def __iter__(self): return self._participants.__iter__() def add(self, participant_id): """Adds a participant by their ID (address).""" self._operation_queue.wavelet_add_participant( self._wave_id, self._wavelet_id, participant_id) self._participants.add(participant_id) def get_role(self, participant_id): """Return the role for the given participant_id.""" return self._roles.get(participant_id, Participants.ROLE_FULL) def set_role(self, participant_id, role): """Sets the role for the given participant_id.""" if role != Participants.ROLE_FULL and role != Participants.ROLE_READ_ONLY: raise ValueError(role + ' is not a valid role') self._operation_queue.wavelet_modify_participant_role( self._wave_id, self._wavelet_id, participant_id, role) self._roles[participant_id] = role def serialize(self): """Returns a list of the participants.""" return list(self._participants) class Tags(object): """Class modelling a list of tags.""" def __init__(self, tags, wave_id, wavelet_id, operation_queue): self._tags = list(tags) self._wave_id = wave_id self._wavelet_id = wavelet_id self._operation_queue = operation_queue def __getitem__(self, index): return self._tags[index] def __len__(self): return len(self._tags) def __iter__(self): return self._tags.__iter__() def append(self, tag): """Appends a tag if it doesn't already exist.""" tag = util.force_unicode(tag) if tag in self._tags: return self._operation_queue.wavelet_modify_tag( self._wave_id, self._wavelet_id, tag) self._tags.append(tag) def remove(self, tag): """Removes a tag if it exists.""" tag = util.force_unicode(tag) if not tag in self._tags: return self._operation_queue.wavelet_modify_tag( self._wave_id, self._wavelet_id, tag, modify_how='remove') self._tags.remove(tag) def serialize(self): """Returns a list of tags.""" return list(self._tags) class BlipThread(object): """ Models a group of blips in a wave.""" def __init__(self, id, location, blip_ids, all_blips, operation_queue): self._id = id self._location = location self._blip_ids = blip_ids self._all_blips = all_blips self._operation_queue = operation_queue @property def id(self): """Returns this thread's id.""" return self._id @property def location(self): """Returns this thread's location.""" return self._location @property def blip_ids(self): """Returns the blip IDs in this thread.""" return self._blip_ids @property def blips(self): """Returns the blips in this thread.""" blips = [] for blip_id in self._blip_ids: blips.append(self._all_blips[blip_id]) return blips def _add_internal(self, blip): """Adds a blip to the thread, sends out no operations.""" self._blip_ids.append(blip.blip_id) self._all_blips[blip.blip_id] = blip def serialize(self): """ Returns serialized properties.""" return {'id': self._id, 'location': self._location, 'blipIds': self._blip_ids} class Wavelet(object): """Models a single wavelet. A single wavelet is composed of metadata, participants, and its blips. To guarantee that all blips are available, specify Context.ALL for events. """ def __init__(self, json, blips, root_thread, operation_queue, raw_deltas=None): """Inits this wavelet with JSON data. Args: json: JSON data dictionary from Wave server. blips: a dictionary object that can be used to resolve blips. root_thread: a BlipThread object containing the blips in the root thread. operation_queue: an OperationQueue object to be used to send any generated operations to. """ self._operation_queue = operation_queue self._root_thread = root_thread self._wave_id = json.get('waveId') self._wavelet_id = json.get('waveletId') self._creator = json.get('creator') self._raw_deltas = raw_deltas self._raw_snapshot = json.get('rawSnapshot') self._creation_time = json.get('creationTime', 0) self._data_documents = DataDocs(json.get('dataDocuments', {}), self._wave_id, self._wavelet_id, operation_queue) self._last_modified_time = json.get('lastModifiedTime') self._participants = Participants(json.get('participants', []), json.get('participantRoles', {}), self._wave_id, self._wavelet_id, operation_queue) self._title = json.get('title', '') self._tags = Tags(json.get('tags', []), self._wave_id, self._wavelet_id, operation_queue) self._raw_data = json self._blips = blip.Blips(blips) self._root_blip_id = json.get('rootBlipId') if self._root_blip_id and self._root_blip_id in self._blips: self._root_blip = self._blips[self._root_blip_id] else: self._root_blip = None self._robot_address = None @property def wavelet_id(self): """Returns this wavelet's id.""" return self._wavelet_id @property def wave_id(self): """Returns this wavelet's parent wave id.""" return self._wave_id @property def creator(self): """Returns the participant id of the creator of this wavelet.""" return self._creator @property def creation_time(self): """Returns the time that this wavelet was first created in milliseconds.""" return self._creation_time @property def data_documents(self): """Returns the data documents for this wavelet based on key name.""" return self._data_documents @property def domain(self): """Return the domain that wavelet belongs to.""" p = self._wave_id.find('!') if p == -1: return None else: return self._wave_id[:p] @property def last_modified_time(self): """Returns the time that this wavelet was last modified in ms.""" return self._last_modified_time @property def participants(self): """Returns a set of participants on this wavelet.""" return self._participants @property def root_thread(self): """Returns the root thread of this wavelet.""" return self._root_thread @property def tags(self): """Returns a list of tags for this wavelet.""" return self._tags @property def raw_deltas(self): """If present, return the raw deltas for this wavelet.""" return self._raw_deltas @property def raw_snapshot(self): """If present, return the raw snapshot for this wavelet.""" return self._raw_snapshot def _get_title(self): return self._title def _set_title(self, title): title = util.force_unicode(title) if title.find('\n') != -1: raise errors.Error('Wavelet title should not contain a newline ' + 'character. Specified: ' + title) self._operation_queue.wavelet_set_title(self.wave_id, self.wavelet_id, title) self._title = title # Adjust the content of the root blip, if it is available in the context. if self._root_blip: content = '\n' splits = self._root_blip._content.split('\n', 2) if len(splits) == 3: content += splits[2] self._root_blip._content = '\n' + title + content #: Returns or sets the wavelet's title. title = property(_get_title, _set_title, doc='Get or set the title of the wavelet.') def _get_robot_address(self): return self._robot_address def _set_robot_address(self, address): if self._robot_address: raise errors.Error('robot address already set') self._robot_address = address robot_address = property(_get_robot_address, _set_robot_address, doc='Get or set the address of the current robot.') @property def root_blip(self): """Returns this wavelet's root blip.""" return self._root_blip @property def blips(self): """Returns the blips for this wavelet.""" return self._blips def get_operation_queue(self): """Returns the OperationQueue for this wavelet.""" return self._operation_queue def serialize(self): """Return a dict of the wavelet properties.""" return {'waveId': self._wave_id, 'waveletId': self._wavelet_id, 'creator': self._creator, 'creationTime': self._creation_time, 'dataDocuments': self._data_documents.serialize(), 'lastModifiedTime': self._last_modified_time, 'participants': self._participants.serialize(), 'title': self._title, 'blips': self._blips.serialize(), 'rootBlipId': self._root_blip_id, 'rootThread': self._root_thread.serialize() } def proxy_for(self, proxy_for_id): """Return a view on this wavelet that will proxy for the specified id. A shallow copy of the current wavelet is returned with the proxy_for_id set. Any modifications made to this copy will be done using the proxy_for_id, i.e. the robot+@appspot.com address will be used. If the wavelet was retrieved using the Active Robot API, that is by fetch_wavelet, then the address of the robot must be added to the wavelet by setting wavelet.robot_address before calling proxy_for(). """ util.check_is_valid_proxy_for_id(proxy_for_id) self.add_proxying_participant(proxy_for_id) operation_queue = self.get_operation_queue().proxy_for(proxy_for_id) res = Wavelet(json={}, blips={}, root_thread=None, operation_queue=operation_queue) res._wave_id = self._wave_id res._wavelet_id = self._wavelet_id res._creator = self._creator res._creation_time = self._creation_time res._data_documents = self._data_documents res._last_modified_time = self._last_modified_time res._participants = self._participants res._title = self._title res._raw_data = self._raw_data res._blips = self._blips res._root_blip = self._root_blip res._root_thread = self._root_thread return res def add_proxying_participant(self, id): """Ads a proxying participant to the wave. Proxying participants are of the form robot+proxy@domain.com. This convenience method constructs this id and then calls participants.add. """ if not self.robot_address: raise errors.Error( 'Need a robot address to add a proxying for participant') robotid, domain = self.robot_address.split('@', 1) if '#' in robotid: robotid, version = robotid.split('#') else: version = None if '+' in robotid: newid = robotid.split('+', 1)[0] + '+' + id else: newid = robotid + '+' + id if version: newid += '#' + version newid += '@' + domain self.participants.add(newid) def submit_with(self, other_wavelet): """Submit this wavelet when the passed other wavelet is submited. wavelets constructed outside of the event callback need to be either explicitly submited using robot.submit(wavelet) or be associated with a different wavelet that will be submited or is part of the event callback. """ other_wavelet._operation_queue.copy_operations(self._operation_queue) self._operation_queue = other_wavelet._operation_queue def reply(self, initial_content=None): """Replies to the conversation in this wavelet. Args: initial_content: If set, start with this (string) content. Returns: A transient version of the blip that contains the reply. """ if not initial_content: initial_content = u'\n' initial_content = util.force_unicode(initial_content) blip_data = self._operation_queue.wavelet_append_blip( self.wave_id, self.wavelet_id, initial_content) instance = blip.Blip(blip_data, self._blips, self._operation_queue) self._blips._add(instance) self.root_blip.child_blip_ids.append(instance.blip_id) return instance def delete(self, todelete): """Remove a blip from this wavelet. Args: todelete: either a blip or a blip id to be removed. """ if isinstance(todelete, blip.Blip): blip_id = todelete.blip_id else: blip_id = todelete self._operation_queue.blip_delete(self.wave_id, self.wavelet_id, blip_id) self._blips._remove_with_id(blip_id)