#!/usr/bin/python # # 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. """Support for operations that can be applied to the server. Contains classes and utilities for creating operations that are to be applied on the server. """ import errors import random import util import sys PROTOCOL_VERSION = '0.22' # Operation Types WAVELET_APPEND_BLIP = 'wavelet.appendBlip' WAVELET_SET_TITLE = 'wavelet.setTitle' WAVELET_ADD_PARTICIPANT = 'wavelet.participant.add' WAVELET_DATADOC_SET = 'wavelet.datadoc.set' WAVELET_MODIFY_TAG = 'wavelet.modifyTag' WAVELET_MODIFY_PARTICIPANT_ROLE = 'wavelet.modifyParticipantRole' BLIP_CONTINUE_THREAD = 'blip.continueThread' BLIP_CREATE_CHILD = 'blip.createChild' BLIP_DELETE = 'blip.delete' DOCUMENT_APPEND_MARKUP = 'document.appendMarkup' DOCUMENT_INLINE_BLIP_INSERT = 'document.inlineBlip.insert' DOCUMENT_MODIFY = 'document.modify' ROBOT_CREATE_WAVELET = 'robot.createWavelet' ROBOT_FETCH_WAVE = 'robot.fetchWave' ROBOT_NOTIFY = 'robot.notify' ROBOT_SEARCH = 'robot.search' # Assign always NOTIFY_OP_ID to the notify operation so # we can easily filter it out later NOTIFY_OP_ID = '0' class Operation(object): """Represents a generic operation applied on the server. This operation class contains data that is filled in depending on the operation type. It can be used directly, but doing so will not result in local, transient reflection of state on the blips. In other words, creating a 'delete blip' operation will not remove the blip from the local context for the duration of this session. It is better to use the OpBased model classes directly instead. """ def __init__(self, method, opid, params): """Initializes this operation with contextual data. Args: method: Method to call or type of operation. opid: The id of the operation. Any callbacks will refer to these. params: An operation type dependent dictionary """ self.method = method self.id = opid self.params = params def __str__(self): return '%s[%s]%s' % (self.method, self.id, str(self.params)) def set_param(self, param, value): self.params[param] = value return self def serialize(self, method_prefix=''): """Serialize the operation. Args: method_prefix: prefixed for each method name to allow for specifying a namespace. Returns: a dict representation of the operation. """ if method_prefix and not method_prefix.endswith('.'): method_prefix += '.' return {'method': method_prefix + self.method, 'id': self.id, 'params': util.serialize(self.params)} def set_optional(self, param, value): """Sets an optional parameter. If value is None or "", this is a no op. Otherwise it calls set_param. """ if value == '' or value is None: return self else: return self.set_param(param, value) class OperationQueue(object): """Wraps the queuing of operations using easily callable functions. The operation queue wraps single operations as functions and queues the resulting operations in-order. Typically there shouldn't be a need to call this directly unless operations are needed on entities outside of the scope of the robot. For example, to modify a blip that does not exist in the current context, you might specify the wave, wavelet and blip id to generate an operation. Any calls to this will not be reflected in the robot in any way. For example, calling wavelet_append_blip will not result in a new blip being added to the robot, only an operation to be applied on the server. """ # Some class global counters: _next_operation_id = 1 def __init__(self, proxy_for_id=None): self.__pending = [] self._capability_hash = None self._proxy_for_id = proxy_for_id def _new_blipdata(self, wave_id, wavelet_id, initial_content='', parent_blip_id=None): """Creates JSON of the blip used for this session.""" temp_blip_id = 'TBD_%s_%s' % (wavelet_id, hex(random.randint(0, sys.maxint))) return {'waveId': wave_id, 'waveletId': wavelet_id, 'blipId': temp_blip_id, 'content': initial_content, 'parentBlipId': parent_blip_id} def _new_waveletdata(self, domain, participants): """Creates an ephemeral WaveletData instance used for this session. Args: domain: the domain to create the data for. participants initially on the wavelet Returns: Blipdata (for the rootblip), WaveletData. """ wave_id = domain + '!TBD_%s' % hex(random.randint(0, sys.maxint)) wavelet_id = domain + '!conv+root' root_blip_data = self._new_blipdata(wave_id, wavelet_id) participants = set(participants) wavelet_data = {'waveId': wave_id, 'waveletId': wavelet_id, 'rootBlipId': root_blip_data['blipId'], 'participants': participants} return root_blip_data, wavelet_data def __len__(self): return len(self.__pending) def __iter__(self): return self.__pending.__iter__() def clear(self): self.__pending = [] def proxy_for(self, proxy): """Return a view of this operation queue with the proxying for set to proxy. This method returns a new instance of an operation queue that shares the operation list, but has a different proxying_for_id set so the robot using this new queue will send out operations with the proxying_for field set. """ res = OperationQueue() res.__pending = self.__pending res._capability_hash = self._capability_hash res._proxy_for_id = proxy return res def set_capability_hash(self, capability_hash): self._capability_hash = capability_hash def serialize(self, method_prefix=''): first = Operation(ROBOT_NOTIFY, NOTIFY_OP_ID, {'capabilitiesHash': self._capability_hash, 'protocolVersion': PROTOCOL_VERSION}) operations = [first] + self.__pending return [op.serialize(method_prefix=method_prefix) for op in operations] res = util.serialize(operations) return res def copy_operations(self, other_queue): """Copy the pending operations from other_queue into this one.""" for op in other_queue: self.__pending.append(op) def new_operation(self, method, wave_id, wavelet_id, props=None, **kwprops): """Creates and adds a new operation to the operation list.""" if props is None: props = {} props.update(kwprops) if wave_id is not None: props['waveId'] = wave_id if wavelet_id is not None: props['waveletId'] = wavelet_id if self._proxy_for_id: props['proxyingFor'] = self._proxy_for_id operation = Operation(method, 'op%s' % OperationQueue._next_operation_id, props) self.__pending.append(operation) OperationQueue._next_operation_id += 1 return operation def wavelet_append_blip(self, wave_id, wavelet_id, initial_content=''): """Appends a blip to a wavelet. Args: wave_id: The wave id owning the containing wavelet. wavelet_id: The wavelet id that this blip should be appended to. initial_content: optionally the content to start with Returns: JSON representing the information of the new blip. """ blip_data = self._new_blipdata(wave_id, wavelet_id, initial_content) self.new_operation(WAVELET_APPEND_BLIP, wave_id, wavelet_id, blipData=blip_data) return blip_data def wavelet_add_participant(self, wave_id, wavelet_id, participant_id): """Adds a participant to a wavelet. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. participant_id: Id of the participant to add. Returns: data for the root_blip, wavelet """ return self.new_operation(WAVELET_ADD_PARTICIPANT, wave_id, wavelet_id, participantId=participant_id) def wavelet_datadoc_set(self, wave_id, wavelet_id, name, data): """Sets a key/value pair on the data document of a wavelet. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. name: The key name for this data. data: The value of the data to set. Returns: The operation created. """ return self.new_operation(WAVELET_DATADOC_SET, wave_id, wavelet_id, datadocName=name, datadocValue=data) def robot_create_wavelet(self, domain, participants=None, message=''): """Creates a new wavelet. Args: domain: the domain to create the wave in participants: initial participants on this wavelet or None if none message: an optional payload that is returned with the corresponding event. Returns: data for the root_blip, wavelet """ if participants is None: participants = [] blip_data, wavelet_data = self._new_waveletdata(domain, participants) op = self.new_operation(ROBOT_CREATE_WAVELET, wave_id=wavelet_data['waveId'], wavelet_id=wavelet_data['waveletId'], waveletData=wavelet_data) op.set_optional('message', message) return blip_data, wavelet_data def robot_search(self, query, index=None, num_results=None): """Execute a search request. For now this only makes sense in the data API. Wave does not maintain an index for robots so no results will be returned in that scenario. Args: query: what to search for index: what index to search from num_results: how many results to return Returns: The operation created. """ op = self.new_operation( ROBOT_SEARCH, wave_id=None, wavelet_id=None, query=query) if index is not None: op.set_param('index', index) if num_results is not None: op.set_param('numResults', num_results) return op def robot_fetch_wave(self, wave_id, wavelet_id, raw_deltas_from_version=-1, return_raw_snapshot=False): """Requests a snapshot of the specified wavelet. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. raw_deltas_from_version: If specified, return a raw dump of the delta history of this wavelet, starting at the given version. This may return only part of the history; use additional requests with higher raw_deltas_from_version parameters to get the rest. return_raw_snapshot: if true, return the raw data for this wavelet. Returns: The operation created. """ op = self.new_operation(ROBOT_FETCH_WAVE, wave_id, wavelet_id) if raw_deltas_from_version != -1: op.set_param('rawDeltasFromVersion', raw_deltas_from_version) if return_raw_snapshot: op.set_param('returnRawSnapshot', return_raw_snapshot) return op def wavelet_set_title(self, wave_id, wavelet_id, title): """Sets the title of a wavelet. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. title: The title to set. Returns: The operation created. """ return self.new_operation(WAVELET_SET_TITLE, wave_id, wavelet_id, waveletTitle=title) def wavelet_modify_participant_role( self, wave_id, wavelet_id, participant_id, role): """Modify the role of a participant on a wavelet. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. participant_id: Id of the participant to add. role: the new roles Returns: data for the root_blip, wavelet """ return self.new_operation(WAVELET_MODIFY_PARTICIPANT_ROLE, wave_id, wavelet_id, participantId=participant_id, participantRole=role) def wavelet_modify_tag(self, wave_id, wavelet_id, tag, modify_how=None): """Modifies a tag in a wavelet. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. tag: The tag (a string). modify_how: (optional) how to apply the tag. The default is to add the tag. Specify 'remove' to remove. Specify None or 'add' to add. Returns: The operation created. """ return self.new_operation(WAVELET_MODIFY_TAG, wave_id, wavelet_id, name=tag).set_optional("modify_how", modify_how) def blip_create_child(self, wave_id, wavelet_id, blip_id): """Creates a child blip of another blip. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. blip_id: The blip id that this operation is applied to. Returns: JSON of blip for which further operations can be applied. """ blip_data = self._new_blipdata(wave_id, wavelet_id, parent_blip_id=blip_id) self.new_operation(BLIP_CREATE_CHILD, wave_id, wavelet_id, blipId=blip_id, blipData=blip_data) return blip_data def blip_continue_thread(self, wave_id, wavelet_id, blip_id): """Creates a blip in same thread as specified blip. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. blip_id: The blip id that this operation is applied to. Returns: JSON of blip for which further operations can be applied. """ blip_data = self._new_blipdata(wave_id, wavelet_id) self.new_operation(BLIP_CONTINUE_THREAD, wave_id, wavelet_id, blipId=blip_id, blipData=blip_data) return blip_data def blip_delete(self, wave_id, wavelet_id, blip_id): """Deletes the specified blip. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. blip_id: The blip id that this operation is applied to. Returns: The operation created. """ return self.new_operation(BLIP_DELETE, wave_id, wavelet_id, blipId=blip_id) def document_append_markup(self, wave_id, wavelet_id, blip_id, content): """Appends content with markup to a document. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. blip_id: The blip id that this operation is applied to. content: The markup content to append. Returns: The operation created. """ return self.new_operation(DOCUMENT_APPEND_MARKUP, wave_id, wavelet_id, blipId=blip_id, content=content) def document_modify(self, wave_id, wavelet_id, blip_id): """Creates and queues a document modify operation The returned operation still needs to be filled with details before it makes sense. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. blip_id: The blip id that this operation is applied to. Returns: The operation created. """ return self.new_operation(DOCUMENT_MODIFY, wave_id, wavelet_id, blipId=blip_id) def document_inline_blip_insert(self, wave_id, wavelet_id, blip_id, position): """Inserts an inline blip at a specific location. Args: wave_id: The wave id owning that this operation is applied to. wavelet_id: The wavelet id that this operation is applied to. blip_id: The blip id that this operation is applied to. position: The position in the document to insert the blip. Returns: JSON data for the blip that was created for further operations. """ inline_blip_data = self._new_blipdata(wave_id, wavelet_id) inline_blip_data['parentBlipId'] = blip_id self.new_operation(DOCUMENT_INLINE_BLIP_INSERT, wave_id, wavelet_id, blipId=blip_id, index=position, blipData=inline_blip_data) return inline_blip_data