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 asyncio 

2import sys 

3import inspect 

4import traceback 

5import time 

6import random 

7from gutools.tools import _call 

8from gutools.colors import * 

9from gutools.uobjects import UObject 

10 

11available_context = dict() 

12 

13def context(**context): 

14 def decorator(f): 

15 @wraps(f) 

16 def wrap(*args, **kw): 

17 message = f(*args, **kw) 

18 return message 

19 

20 f.__context__ = context 

21 key = f.__qualname__.lower() 

22 available_context[key] = f 

23 return wrap 

24 return decorator 

25 

26class UProcedure(UObject): 

27 """A single method that runs (forever usually) 

28 that user can set a policy behavior on 

29 exceptions or external events """ 

30 __args__ = ('method', ) 

31 __kwargs__ = {'exceptions': dict, 'events': dict,} 

32 __slots__ = __args__ + tuple(set(__kwargs__).difference(__args__)) 

33 

34class UApplication(object): 

35 """Supervise some procedures as a whole entity 

36 handling exceptions and events that 

37 happens within them. 

38 

39 UApplication define a Policy for handling 

40 non-expected exceptions. 

41 

42 When an exception is raised, App tries to find a handler for the exception 

43 and will take actions based on handler returned value. 

44 

45 """ 

46 ABORT = 0 

47 RESTART = 1 

48 CONTINUE = 2 

49 

50 def __init__(self, context=None, fixtures=None, procedures=None, 

51 policy=1, exceptions=None, restart_delay=5, 

52 hub=None, tl=None, config=None, session=None, 

53 realm='/default'): 

54 

55 self.context = context or dict() 

56 self.fixtures = fixtures or dict() 

57 self.procedures = procedures or list() 

58 self.policy = policy 

59 self.exceptions = exceptions or dict() 

60 self.restart_delay = restart_delay 

61 self.hub = hub 

62 self.tl = tl 

63 self.realm = realm 

64 self.config = config or dict() 

65 self.session = session or dict() 

66 self.agents = dict() 

67 self.running = False 

68 self._loop = None 

69 

70 async def run(self, *procedures): 

71 """ 

72 TODO: Implement additional policies 

73 - CONTINUE 

74 - RESTART + REMOVE failling procedure 

75 """ 

76 self.running = True 

77 loop = self._loop 

78 assert isinstance(loop, asyncio.base_events.BaseEventLoop) 

79 

80 # assert on using the same loop 

81 assert loop == self.hub._loop 

82 # assert loop == self.tl._loop 

83 

84 self.procedures.extend(procedures) 

85 

86 def collect_awaitables(): 

87 # prepare fixtures 

88 context = self._expand_needed_fixtures() 

89 context.update(self.context) 

90 

91 # collect and fire procedures 

92 awaitables = list() 

93 for proc in self.procedures: 

94 if not inspect.iscoroutinefunction(proc): 

95 raise RuntimeError(f"{proc} is not an coroutine function (async def xxx()") 

96 

97 aw = _call(proc, **context) 

98 awaitables.append(aw) 

99 return awaitables 

100 

101 # create the related tasks 

102 ct = loop.create_task 

103 awaitables = [ct(c) for c in collect_awaitables()] 

104 done = [] 

105 while awaitables: 

106 try: 

107 # get the global policy, that may change by specific exception 

108 policy = self.policy 

109 

110 # fire procedures waiting to end or any exception 

111 done, pending = await asyncio.wait(awaitables, loop=loop, 

112 return_when=asyncio.FIRST_COMPLETED) 

113 except Exception as why: 

114 print(f"{RED}{why}{RESET}") 

115 frame = inspect.trace()[-1] 

116 # traceback.print_exc() 

117 # traceback.print_exc(file=sys.stdout) 

118 # print(f"{RESET}") 

119 

120 # Find a handler for the exception 

121 # (per-exception policy) 

122 for ex, p in self.exceptions.items(): 

123 if isinstance(why, ex): 

124 if not isinstance(p, int): 

125 policy = p(why) 

126 break 

127 # acts according policy 

128 if policy in (self.ABORT, ): 

129 break 

130 elif policy in (self.RESTART, ): 

131 # TODO: change loop to make it reentrant 

132 print(f"{YELLOW}Restarting finished tasks in {self.restart_delay} sec{RESET}") 

133 await asyncio.sleep(self.restart_delay) 

134 for i, coro in enumerate(collect_awaitables()): 

135 if awaitables[i] in done: 

136 # replace the finished task by a new one 

137 awaitables[i] = ct(coro) 

138 

139 elif policy in (self.CONTINUE, ): 

140 raise NotImplementedError("CONTINUE policy is not yet Implemented") 

141 

142 async def start(self): 

143 self._loop = asyncio.get_running_loop() # must be already running 

144 # self._loop = asyncio.get_event_loop() 

145 assert self._loop.is_running() 

146 await self.hub.start(self._loop) 

147 # self.tl.start(self._loop) # TODO: 

148 

149 async def stop(self): 

150 await self.hub.stop() 

151 # self.tl.stop() 

152 

153 def pause(self): 

154 pass 

155 

156 def resume(self): 

157 pass 

158 

159 def add_agent(self, factory, **kw): 

160 args = self._expand_args(factory, **kw) 

161 agent = _call(factory, **args) 

162 self.agents[agent.uid] = agent 

163 return agent 

164 

165 def _expand_needed_fixtures(self): 

166 "Expand the arguments needed by awaitables from fixtured factories" 

167 exp = dict() 

168 for func in self.procedures: 

169 exp.update(self._expand_args(func)) 

170 return exp 

171 

172 def _expand_args(self, func, **kw): 

173 kw2 = dict(self.fixtures) 

174 kw2.update(kw) 

175 

176 exp = dict() 

177 info = inspect.getfullargspec(func) 

178 for arg in info.args: 

179 if arg in kw2: 

180 fix = kw2[arg] 

181 if hasattr(fix, '__call__'): 

182 fix = _call(fix, **self.context) 

183 exp[arg] = fix 

184 

185 return exp 

186 

187 

188def test_uobject_factories(): 

189 p1 = UProcedure() 

190 p1.events['foo'] = 1 

191 

192 p2 = UProcedure() 

193 p2.exceptions['bar'] = 1 

194 

195 p3 = UProcedure() 

196 

197 assert 'foo' in p1.events 

198 assert 'foo' not in p2.events 

199 assert 'foo' not in p3.events 

200 

201 assert 'bar' not in p1.exceptions 

202 assert 'bar' in p2.exceptions 

203 assert 'bar' not in p3.exceptions 

204 

205 

206def test_uapplication(): 

207 

208 async def foo(i): 

209 print(f"> Enter foo({i})") 

210 await asyncio.sleep(2) 

211 print(f"< Exit foo({i})") 

212 

213 async def bar(i): 

214 print(f"> Enter bar({i})") 

215 await asyncio.sleep(3) 

216 _ = 1 / 0.0 

217 print(f"< Exit bar({i})") 

218 

219 async def buz(i): 

220 print(f"> Enter buz({i})") 

221 await asyncio.sleep(1) 

222 if random.random() < 0.25: 

223 open('/non-existin-directory', 'r') 

224 print(f"< Exit buz({i})") 

225 

226 def exception_handler(exc): 

227 if random.random() > 0.8: 

228 return UApplication.ABORT 

229 if random.random() > 0.5: 

230 # return UApplication.CONTINUE 

231 return UApplication.RESTART 

232 return UApplication.RESTART 

233 

234 app = UApplication(policy=UApplication.RESTART) 

235 app.procedures = [foo, bar, buz] 

236 app.exceptions[ZeroDivisionError] = app.RESTART 

237 app.exceptions[FileNotFoundError] = exception_handler 

238 

239 app.run() 

240 

241 foo = 1 

242 

243if __name__ == '__main__': 

244 test_uobject_factories() 

245 test_uapplication() 

246 

247 

248 foo = 1