blob: c3352e45c14c61a5f7dc78543d364338191e0f4c [file] [log] [blame]
#!/usr/bin/python2.4
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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+<proxy_for_id>@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)