Coverage for /home/agp/Documents/me/code/gutools/gutools/uagents.py : 0%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2import types
3import asyncio
4from functools import wraps
5from aiopb.aiopb import Hub
7# TODO: move UAgent to gutools or aiopb
8from gutools.tools import _call
10# hub = get_hub()
11exposed = dict()
12keys_functions = dict()
13members = dict()
14def expose(**context):
15 def decorator(f):
16 @wraps(f)
17 def wrap(*args, **kw):
18 message = f(*args, **kw)
19 if message is not None:
20 key = f.__context__.get('response')
21 if key is None:
22 key = '{}/{}'.format(keys_functions[f], 'response')
23 hub.publish(key, message)
24 return message
26 assert f.__name__[0] != '_', "Exposing private member ({}) is not allowed".format(f.__name__)
27 f.__context__ = context
28 key = '/uagent/{}'.format(f.__qualname__.lower().replace('.', '/'))
29 exposed[key] = f
30 keys_functions[f] = key
31 return wrap
32 return decorator
34def domestic(condition='True', start=0, restart=5, **env):
35 """Decorator for domestic method of a UAgent class.
37 Domestic are implemented by asyncio module.
38 """
39 def decorator(f):
40 @wraps(f)
41 def wrap(*args, **kw):
42 if Domestic.stop_restart:
43 self_1 = args[0]
44 for candidate in list(Domestic.stop_restart.keys()):
45 if candidate.__func__ == wrap:
46 if candidate.__self__ == self_1:
47 Domestic.stop_restart.pop(candidate)
48 return
49 else:
50 foo = 1
52 start, condition, restart, env = wrap.__domestic__
53 kw2 = dict(env)
54 kw2.update(kw)
56 if eval(condition, {'self': args[0],}, kw2):
57 f(*args, **kw2)
59 if restart > 0:
60 loop = asyncio.get_event_loop()
61 loop.call_later(restart, wrap, *args,)
63 wrap.__domestic__ = (start, compile(condition, '<string>', 'eval'), restart, env)
64 wrap.__domestic__backup__ = wrap.__domestic__
65 return wrap
66 return decorator
68def update_domestic(wrap, **env):
69 func = wrap.__func__
70 existing = list(func.__domestic__)
71 for i, key in enumerate(['start', 'condition', 'restart']):
72 value = env.pop(key, None)
73 if value is not None:
74 if key == 'condition':
75 value = compile(value, '<string>', 'eval')
76 existing[i] = value
78 existing[-1].update(env)
79 func.__domestic__ = tuple(existing)
81def restore_domestic(wrap, **env):
82 func = wrap.__func__
83 func.__domestic__ = func.__domestic__backup__
85class Domestic(object):
86 """Support for domestic methods.
87 Any derived class may decorate a function member with
89 @domestic(condition='True', start=0, restart=5, **env)
91 and the asyncio loop will call when params match.
92 """
93 stop_restart = dict()
95 def __init__(self):
96 # add to schedule all domestic taks
97 loop = asyncio.get_event_loop()
98 for name in dir(self):
99 func = getattr(self, name)
100 if isinstance(func, types.MethodType):
101 params = getattr(func, '__domestic__', None)
102 if params:
103 loop.call_later(params[0] or 0, func, )
105 def __del__(self):
106 self.stop_domestics()
108 def stop_domestics(self):
109 for name in dir(self):
110 func = getattr(self, name)
111 if isinstance(func, types.MethodType):
112 params = getattr(func, '__domestic__', None)
113 if params:
114 self._stop_domestic(func)
116 def _stop_domestic(self, func):
117 self.stop_restart[func] = self
120class UAgent(object):
121 """Allow to handle (key, messages) requests.
123 Subscribe to '/uagent/(class|uid)' channels in order to receive requests
124 from anywhere inside the same realm.
126 Any @expose function will be accessible if key match the funcion name
127 The keys must have the format:
128 uagent/method
129 uagent/member/method
131 agent instances or members are cached for next calls with same key.
133 _call() will try to match arguments from message to function
134 before make the call and strategy used will cache when possible.
136 Finally, if `__reply_key__` is provided in the message (dict subclase),
137 the response will be published into this channel.
138 """
140 def __init__(self, uid=None, app=None):
141 super().__init__()
142 kname = self.__class__.__name__.lower()
143 self.uid = uid or kname
144 assert self.uid
145 self.pattern = '/uagent/({}|{})/(?P<func>.*)$'.format(kname, self.uid)
146 # e.g. uagent/dbevents/foo_func
147 self.app = app
148 self.app.hub.subscribe(self.pattern, self.handle)
150 def __enter__(self):
151 return self
153 def __exit__(self, exception_type, exception_value, traceback):
154 if exception_type is None:
155 self.__detach__()
157 def __detach__(self):
158 hub = self.app.hub
159 hub and hub.detach(self)
161 def __del__(self):
162 self.__detach__()
164 def handle(self, key, message):
165 # TODO: review if
166 func, _self = exposed.get(key), members.get(self, dict()).get(key, self)
167 if not func:
168 m = re.match(self.pattern, key, re.I|re.DOTALL)
169 if m:
170 func, _self = self, None
171 for locator in m.groups()[0].split('/'):
172 if locator[0] == '_':
173 return # don't allow private elements
174 func, _self = getattr(func, locator), func
175 if func:
176 exposed[key] = func # save in cache for next call
177 members.setdefault(self, dict())[key] = _self
179 response = func and _call(func, message, self=_self)
180 if isinstance(message, dict):
181 reply_key = message.get('__reply_key__')
182 if reply_key:
183 self.app.hub.publish(reply_key, response)
184 return response
187def test_call_exposed_methods():
189 agent = FooAgent()
190 r1 = agent.mysum(1, 2)
191 for message in [(1, 2), dict(a=1, b=2)]:
192 r = agent.handle('/uagent/fooagent/mysum', message)
193 assert r == r1
195def test_subrogate_call_to_members():
197 agent = FooAgent()
198 r1 = agent.mysum(1, 2)
199 for message in [(1, 2), dict(a=1, b=2)]:
200 r = agent.handle('uagent/fooagent/calc/sum2', message)
201 assert r == r1
204if __name__ == '__main__':
205 class Calc:
206 def sum2(self, a, b):
207 return a + b
209 class FooAgent(UAgent):
210 def __init__(self):
211 super(FooAgent, self).__init__()
212 self.calc = Calc()
214 @expose(response='foo/bar')
215 def mysum(self, a, b):
216 ""
217 return a + b
219 test_call_exposed_methods()
220 test_subrogate_call_to_members()
222 foo = 1