Hide keyboard shortcuts

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 

6 

7# TODO: move UAgent to gutools or aiopb 

8from gutools.tools import _call 

9 

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 

25 

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 

33 

34def domestic(condition='True', start=0, restart=5, **env): 

35 """Decorator for domestic method of a UAgent class. 

36 

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 

51 

52 start, condition, restart, env = wrap.__domestic__ 

53 kw2 = dict(env) 

54 kw2.update(kw) 

55 

56 if eval(condition, {'self': args[0],}, kw2): 

57 f(*args, **kw2) 

58 

59 if restart > 0: 

60 loop = asyncio.get_event_loop() 

61 loop.call_later(restart, wrap, *args,) 

62 

63 wrap.__domestic__ = (start, compile(condition, '<string>', 'eval'), restart, env) 

64 wrap.__domestic__backup__ = wrap.__domestic__ 

65 return wrap 

66 return decorator 

67 

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 

77 

78 existing[-1].update(env) 

79 func.__domestic__ = tuple(existing) 

80 

81def restore_domestic(wrap, **env): 

82 func = wrap.__func__ 

83 func.__domestic__ = func.__domestic__backup__ 

84 

85class Domestic(object): 

86 """Support for domestic methods. 

87 Any derived class may decorate a function member with 

88 

89 @domestic(condition='True', start=0, restart=5, **env) 

90 

91 and the asyncio loop will call when params match. 

92 """ 

93 stop_restart = dict() 

94 

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, ) 

104 

105 def __del__(self): 

106 self.stop_domestics() 

107 

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) 

115 

116 def _stop_domestic(self, func): 

117 self.stop_restart[func] = self 

118 

119 

120class UAgent(object): 

121 """Allow to handle (key, messages) requests. 

122 

123 Subscribe to '/uagent/(class|uid)' channels in order to receive requests 

124 from anywhere inside the same realm. 

125 

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 

130 

131 agent instances or members are cached for next calls with same key. 

132 

133 _call() will try to match arguments from message to function 

134 before make the call and strategy used will cache when possible. 

135 

136 Finally, if `__reply_key__` is provided in the message (dict subclase), 

137 the response will be published into this channel. 

138 """ 

139 

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) 

149 

150 def __enter__(self): 

151 return self 

152 

153 def __exit__(self, exception_type, exception_value, traceback): 

154 if exception_type is None: 

155 self.__detach__() 

156 

157 def __detach__(self): 

158 hub = self.app.hub 

159 hub and hub.detach(self) 

160 

161 def __del__(self): 

162 self.__detach__() 

163 

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 

178 

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 

185 

186 

187def test_call_exposed_methods(): 

188 

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 

194 

195def test_subrogate_call_to_members(): 

196 

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 

202 

203 

204if __name__ == '__main__': 

205 class Calc: 

206 def sum2(self, a, b): 

207 return a + b 

208 

209 class FooAgent(UAgent): 

210 def __init__(self): 

211 super(FooAgent, self).__init__() 

212 self.calc = Calc() 

213 

214 @expose(response='foo/bar') 

215 def mysum(self, a, b): 

216 "" 

217 return a + b 

218 

219 test_call_exposed_methods() 

220 test_subrogate_call_to_members() 

221 

222 foo = 1