Coverage for /home/runner/work/coxbuild/coxbuild/src/coxbuild/services.py: 75%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import asyncio
2import inspect
3import logging
4import sys
5import traceback
6from dataclasses import dataclass, field
7from typing import TYPE_CHECKING, Any, AsyncIterator, Awaitable, Callable
9from coxbuild.configurations import Configuration
10from coxbuild.exceptions import (CoxbuildRuntimeException,
11 CoxbuildSchemaException)
12from coxbuild.runners import Runner
13from coxbuild.runtime import ExecutionState
14from coxbuild.tasks import Task, named, task
16if TYPE_CHECKING:
17 from .extensions import Extension
19logger = logging.getLogger("services")
22class EventFailed(CoxbuildRuntimeException):
23 """Event failed."""
24 pass
27class EventHandlerFailed(CoxbuildRuntimeException):
28 """Event handler failed."""
29 pass
32@dataclass
33class EventContext:
34 """Event occur information."""
35 args: list[Any] = field(default_factory=list)
36 """arguments"""
37 kwds: dict[str, Any] = field(default_factory=dict)
38 """keyword arguments"""
40 @classmethod
41 def build(cls, *args: Any, **kwds: Any) -> "EventContext":
42 return EventContext(args, kwds)
45EventType = AsyncIterator[EventContext | None]
48@dataclass
49class EventHandler:
50 """Handler for a event."""
51 event: EventType
52 """event generator, when the event occurs, the awaitable return"""
53 handler: Task
54 """event handler"""
55 safe: bool = False
56 """prevent exception"""
57 name: str = ""
58 """handler name"""
59 extension: "Extension | None" = None
61 async def handle(self, config: Configuration | None = None):
62 logger.debug(f"Handle for event: {self.name}.")
64 config = config or Configuration()
65 executionState = ExecutionState(config)
67 try:
68 async for context in self.event:
69 oldhandler = executionState.handler
70 oldevent = executionState.event
72 executionState.handler = self
73 executionState.event = context
75 logger.debug(f"Event occurs: {self.name}({context}).")
77 logger.debug(f"Event handling: {self.name}({context}).")
79 try:
80 runner = self.handler(
81 *context.args, **context.kwds) if context else self.handler()
82 runner.context.config = config
83 result = await runner
84 except Exception as ex:
85 if self.safe:
86 print(
87 f"Exception when event handler handling {self.name}({context}).")
88 traceback.print_exception(ex, file=sys.stdout)
89 else:
90 raise EventHandlerFailed(
91 f"Exception when event handler handling {self.name}({context})", ex)
92 finally:
93 executionState.handler = oldhandler
94 executionState.event = oldevent
96 logger.debug(f"Event handled: {self.name}({context}).")
97 except Exception as ex:
98 logger.error(
99 f"Event handler '{self.name}' failed.", exc_info=ex)
101 if self.safe:
102 print(f"Exception in event handler {self.name}.")
103 traceback.print_exception(ex, file=sys.stdout)
104 else:
105 raise EventFailed(
106 f"Exception in event handler {self.name}", ex)
108 logger.debug(f"Finish handle for event: {self.name}.")
111@dataclass
112class Service:
113 """Event-based service."""
115 handlers: list[EventHandler] = field(default_factory=list)
116 """handlers in the service"""
118 def copy(self) -> "Service":
119 """Copy the service."""
120 return Service(self.handlers.copy())
122 def register(self, handler: EventHandler):
123 """Register handler."""
124 self.handlers.append(handler)
125 logger.debug(f"Register event handler: {handler}")
127 def __call__(self, *args: Any, **kwds: Any) -> Any:
128 return ServiceRunner(self)
131@dataclass
132class ServiceContext:
133 """Service context."""
134 service: Service
135 """service"""
136 config: Configuration | None = None
137 """configuration"""
140class ServiceRunner(Runner):
141 def __init__(self, service: Service) -> None:
142 self.service = service
143 self.context = ServiceContext(self.service)
144 super().__init__(self._run)
146 async def _run(self):
147 logger.debug("Run service.")
148 await asyncio.gather(*[e.handle(self.context.config) for e in self.service.handlers])
149 logger.debug("Finish service.")
152def on(event: Callable[[], Awaitable], safe: bool = False, name: str | None = None):
153 """
154 Decorator to register event handler.
156 event: event generator, when the event occurs, the awaitable return
157 handler: event handler
158 repeat: repeat times, 0 for no-repeat, positive integer for finite repeat, negative integer for infinite repeat
159 safe: prevent exception
160 name: handler name, None to use function name
161 """
162 def decorator(handler: Callable[[], None] | Task | EventHandler | list[EventHandler]) -> EventHandler | list[EventHandler]:
163 if isinstance(handler, EventHandler):
164 tname = name or handler.name
165 tk = handler.handler
166 elif isinstance(handler, list):
167 if len(handler) == 0:
168 raise CoxbuildSchemaException("No event handler in list.")
169 tname = name or handler[0].name
170 tk = handler[0].handler
171 elif isinstance(handler, Task):
172 tname = name or handler.name
173 tk = handler
174 else:
175 tname = name or handler.__name__
176 tk = named(tname)(task(handler))
178 eh = EventHandler(event, tk, safe, tname)
180 if isinstance(handler, EventHandler):
181 return [eh, handler]
182 elif isinstance(handler, list):
183 handler.append(eh)
184 return handler
185 else:
186 return eh
188 return decorator