Coverage for /home/agp/Documents/me/code/gutools/gutools/uapplication.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 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
11available_context = dict()
13def context(**context):
14 def decorator(f):
15 @wraps(f)
16 def wrap(*args, **kw):
17 message = f(*args, **kw)
18 return message
20 f.__context__ = context
21 key = f.__qualname__.lower()
22 available_context[key] = f
23 return wrap
24 return decorator
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__))
34class UApplication(object):
35 """Supervise some procedures as a whole entity
36 handling exceptions and events that
37 happens within them.
39 UApplication define a Policy for handling
40 non-expected exceptions.
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.
45 """
46 ABORT = 0
47 RESTART = 1
48 CONTINUE = 2
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'):
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
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)
80 # assert on using the same loop
81 assert loop == self.hub._loop
82 # assert loop == self.tl._loop
84 self.procedures.extend(procedures)
86 def collect_awaitables():
87 # prepare fixtures
88 context = self._expand_needed_fixtures()
89 context.update(self.context)
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()")
97 aw = _call(proc, **context)
98 awaitables.append(aw)
99 return awaitables
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
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}")
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)
139 elif policy in (self.CONTINUE, ):
140 raise NotImplementedError("CONTINUE policy is not yet Implemented")
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:
149 async def stop(self):
150 await self.hub.stop()
151 # self.tl.stop()
153 def pause(self):
154 pass
156 def resume(self):
157 pass
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
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
172 def _expand_args(self, func, **kw):
173 kw2 = dict(self.fixtures)
174 kw2.update(kw)
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
185 return exp
188def test_uobject_factories():
189 p1 = UProcedure()
190 p1.events['foo'] = 1
192 p2 = UProcedure()
193 p2.exceptions['bar'] = 1
195 p3 = UProcedure()
197 assert 'foo' in p1.events
198 assert 'foo' not in p2.events
199 assert 'foo' not in p3.events
201 assert 'bar' not in p1.exceptions
202 assert 'bar' in p2.exceptions
203 assert 'bar' not in p3.exceptions
206def test_uapplication():
208 async def foo(i):
209 print(f"> Enter foo({i})")
210 await asyncio.sleep(2)
211 print(f"< Exit foo({i})")
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})")
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})")
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
234 app = UApplication(policy=UApplication.RESTART)
235 app.procedures = [foo, bar, buz]
236 app.exceptions[ZeroDivisionError] = app.RESTART
237 app.exceptions[FileNotFoundError] = exception_handler
239 app.run()
241 foo = 1
243if __name__ == '__main__':
244 test_uobject_factories()
245 test_uapplication()
248 foo = 1