# Copyright (c) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany). # Copyright (c) 2001-2005 LOGILAB S.A. (Paris, FRANCE). # # http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Narval's main memory :version: $Revision:$ :author: Logilab :copyright: 2001-2005 LOGILAB S.A. (Paris, FRANCE) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany) :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com """ __revision__ = "$Id: Memory.py,v 1.20 2002/10/14 14:24:11 syt Exp $" __docformat__ = "restructuredtext en" import time import traceback from cStringIO import StringIO from narval import engine_interfaces from narval.tags import clear_tags from narval.recipe import RecipeElement from narval.action import ActionElement from narval.plan import PlanElement from narval.public import AL_NS, implements, \ multi_match_expression from narval.interfaces.core import IError, IMessage from narval.elements import create_error from narval.elements.core import StartPlanElement, GroupAliasElement def mk_time_stamp(): """return a time stamp :rtype: float :return: time representation for now """ return time.time() class Memory: """narval's main memory, holds a set of elements, derived from `narval.public.ALElement` :type narval: `narval.engine.Narval` :ivar narval: the narval interpreter :type elements: dict :ivar elements: elements in memory, indexed by their eid :type actions: dict :ivar actions: eid of actions (`narval.action.ActionElement`) in memory, indexed by their identity (.) :type recipes: dict :ivar recipes: eid of recipes (`narval.recipe.RecipeElement`) in memory, indexed by their identity (.) :type plans: list :ivar plans: list of plans (`narval.plan.PlanElement`) in memory :type eid_count: int :ivar eid_count: eid counter :type eid_ref_count: dict :ivar eid_ref_count: references counter : keys are eids and values number of references to the element with the given eid """ __implements__ = (engine_interfaces.ITransitionChangeListener, engine_interfaces.IStepChangeListener, engine_interfaces.IPlanChangeListener) def __init__(self, narval=None) : self.narval = narval self.elements = {} self.actions, self.recipes = {}, {} self._aliases = {} self.plans = [] self.eid_count = 1 self.eid_ref_count = {} self.registry = self.narval.registry def get_element(self, eid): """return the element with the given eid :type eid: int :param eid: the unique identifier of the element :rtype: `narval.public.ALElement` or None :return: the element or None if the given eid is not found """ return self.elements.get(eid) def get_elements(self) : """return all elements in memory :rtype: list :return: all elements in memory """ return self.elements.values() def get_recipe(self, recipe_name) : """return the recipe with the given name :type recipe_name: str :param recipe_name: . :rtype: `narval.recipe.RecipeElement` :return: the recipe element or None if the given name is not found """ return self.recipes[self._key_from_name(recipe_name)] def get_recipes(self): """return all recipe elements in memory :rtype: list :return: all recipe elements in memory """ return self.recipes.values() def get_action(self, name) : """return the action with the given name :type name: str :param name: . :rtype: `narval.action.ActionElement` :return: the action element or None if the given name is not found """ return self.actions[self._key_from_name(name)] def _key_from_name(self, name): group, name = name.split('.') try: group = self._aliases[group] except KeyError: pass return group, name def get_actions(self): """return all action elements in memory :rtype: list :return: all action elements in memory """ return self.actions.values() def as_xml(self, encoding='UTF-8'): """return the memory's content as an xml string :type encoding: str :param encoding: the encoding to use in the returned string, default to UTF-8 :rtype: str :return: the memory as an XML document """ stream = StringIO() write = stream.write write('\n\n' % ( encoding, AL_NS)) for elmt in self.elements.values(): write(elmt.as_xml(encoding)) write('\n') write('') return stream.getvalue() # memory manipulation methods ############################################## def add_message_as_string(self, msg_str): """add a message as xml string to the memory a message is sent by others assistants :type msg_str: str :param msg_str: the XML document containing the message (see narval's DTD for the message's syntax) """ element = self.registry.from_string(msg_str)[0] if not implements(element, IMessage): log(LOG_ERR, "received a false message, dropped it") else: element.type = 'incoming' self.add_element(element) def add_element_as_string(self, element_str): """add an element as xml string to the memory :type element_str: str :param element_str: the XML document containing the element """ self.add_element(self.registry.from_string(element_str)[0]) def replace_element_as_string(self, eid, element_str): """replace an element with a given eid by the element contained in a xml string :type eid: int :param eid: the identifier of the element to replace :type element_str: str :param element_str: the XML document containing the new element """ try: element = self.elements[eid] new_element = self.registry.from_string(element_str)[0] self.replace_element(element, new_element) except KeyError : log(LOG_WARN, 'No element with eid %s in memory', eid) def add_elements(self, elements): """add the given elements list to the memory :type elements: list :param elements: list of element to add in memory """ map(self.add_element, elements) def add_element(self, element): """add an element to memory if the element has already an eid, just increment its reference counter else, (ie yet in memory) assign unique id to the element (eid) and append it to memory. :type element: `narval.public.ALElement` :param element: the element to add in memory :rtype: tuple :return: the eid of the element and the element itself """ if element.eid: eid = element.eid self.eid_ref_count[eid] += 1 else : # assign new eid to element element.eid = eid = self.eid_count self.eid_count += 1 self.eid_ref_count[eid] = 1 element.timestamp = mk_time_stamp() # do some caching processing self.elements[eid] = element if isinstance(element, GroupAliasElement): self._aliases[element.name] = element.actual elif isinstance(element, PlanElement): self.plans.append(element) elif isinstance(element, RecipeElement): self.recipes[element.get_identity()] = element elif isinstance(element, ActionElement): self.actions[element.get_identity()] = element elif self.narval.debug and implements(element, IError): log(LOG_DEBUG, element.as_xml(self.narval.encoding)) element.memory = self # element added to memory: fire event. self.narval.memory_change('add', element) # this new element may trigger a transition. propagate. for plan in self.plans: plan.element_change(element) return eid, element def replace_element(self, old_element, new_element): """replace an element in memory :type old_element: `narval.public.ALElement` :param old_element: the element that should be replaced :type new_element: `narval.public.ALElement` :param new_element: the new element """ new_element.eid = old_element.eid new_element.memory = self self.elements[old_element.eid] = new_element if isinstance(old_element, PlanElement): self.plans.remove(old_element) elif isinstance(old_element, RecipeElement): del self.recipes[old_element.get_identity()] elif isinstance(old_element, ActionElement): del self.actions[old_element.get_identity()] if isinstance(new_element, PlanElement): self.plans.append(new_element) elif isinstance(new_element, RecipeElement): self.recipes[new_element.get_identity()] = new_element elif isinstance(new_element, ActionElement): self.actions[new_element.get_identity()] = new_element self.narval.memory_change('replace', new_element, old_element) # FIXME: this new element may trigger a transition. propagate ? def remove_element(self, element): """remove an element from memory persistent or outdated elements are actually not removed :type element: `narval.public.ALElement` :param element: the element that should be removed """ if element.outdated or not element.persist: clear_tags(element) self.narval.memory_change('remove', element) del self.elements[element.eid] if isinstance(element, PlanElement): self.plans.remove(element) element.cleanup() elif isinstance(element, RecipeElement): del self.recipes[element.get_identity()] elif isinstance(element, ActionElement): del self.actions[element.get_identity()] del element def remove_element_by_id(self, eid): """remove the element with the given eid from the memory :type eid: int :param eid: the identifier of the element to remove """ try: self.remove_element(self.elements[eid]) except KeyError : log(LOG_WARN, 'no element with eid %s in memory', eid) def delete_ref_to_element(self, element): """decrement the reference counter for the given element :type element: `narval.public.ALElement` :param element: element that should be "decrefed" """ eid = element.eid self.eid_ref_count[eid] -= 1 if self.eid_ref_count[eid] < 1: self.remove_element(element) def references_count(self, eid): """return the number of references on the element with the given eid""" return self.eid_ref_count[eid] # memory facilities ######################################################## def are_active_plans(self) : """return true if there are some active plans in memory :rtype: bool :return: flag indicating whether there are or not some active plans in memory """ for plan in self.plans: if plan.state not in ('done', 'failed'): return True return False def mk_error(self, msg, err_type=None) : """build an error element :type msg: str :param msg: the error's message :type err_type: str or None :param err_type: optional error's type :rtype: IError :return: an error element with the given type / msg """ return create_error(msg, err_type) def mk_traceback_error(self, plan, info) : """build an error element from a python traceback :type plan: plan :param plan: the originate plan of the error :type info: tuple :param info: the result of sys.exc_info(), ie a tuple (ex class, ex instance, traceback) :rtype: IError :return: an error element with the given type / msg """ log(LOG_ERR, 'error in plan %s', plan.eid) exc_class, value, tbck = info buf = StringIO() traceback.print_exception(exc_class, value, tbck, file=buf) tb_string = buf.getvalue() error = self.mk_error(tb_string, exc_class) return error def get_inputs(self, input, context=None): """return elements in memory matching the given input prototype, after having incremented their reference counter :type input: `narval.prototype.InputEntry` :param input: the prototype input looking for elements :rtype: list :return: matching elements """ cands = input.match_elements(self.elements.values(), context) # FIXME (syt): i think the commented code below introduce a memory leak, # since elements retreived via this method are also inc-refed when # the plan.add_elements call memory.add_elements, while plan.cleanup # only remove a single reference # #for cand in cands: # self.eid_ref_count[cand.eid] += 1 return cands def match_elements(self, match): """return elements corresponding to the given expression :type match: str :param match: the expression to match :rtype: list :return: matching elements """ return multi_match_expression(match, self.elements.values()) def transition_wait_time(self, transition, date): """schedule a time condition for a transition :type transition: `narval.plan.PlanTransition` :param transition: the transition with a time condition :type date: float :param date: the absolute date of the time condition """ self.narval.schedule_event(('time_condition', transition), when=date, date=True) # proxy for engine ######################################################### def step_change(self, step) : """see `narval.engine.Narval.step_change`""" self.narval.step_change(step) def plan_change(self, plan, action='state', element=None) : """see `narval.engine.Narval.plan_change`""" self.narval.plan_change(plan, action, element) def transition_change(self, transition) : """see `narval.engine.Narval.transition_change`""" self.narval.transition_change(transition) def start_plan(self, recipe_name, parent_plan, parent_step, elements): """see `narval.engine.Narval.start_plan`""" start_plan = StartPlanElement(recipe=recipe_name) start_plan.parent_plan = parent_plan start_plan.parent_step = parent_step start_plan.context = elements self.narval.start_plan(start_plan) def check_date(self, element): """see `narval.engine.Narval.check_date`""" return self.narval.check_date(element) def create_thread(self, *args, **kwargs) : """see `narval.engine.Narval.create_thread`""" self.narval.create_thread(*args, **kwargs)