python-library-observer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
observer/__init__.py
ADDED
observer/bus.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from threading import RLock
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .context import ObserverContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ObserverCallback = Callable[[ObserverContext], None]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ObserverBus:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._callbacks: list[tuple[ObserverCallback, dict[str, Any]]] = []
|
|
16
|
+
self._lock = RLock()
|
|
17
|
+
|
|
18
|
+
def subscribe(
|
|
19
|
+
self,
|
|
20
|
+
callback: ObserverCallback,
|
|
21
|
+
**filters: Any,
|
|
22
|
+
) -> ObserverCallback:
|
|
23
|
+
with self._lock:
|
|
24
|
+
exists = any(
|
|
25
|
+
registered_callback is callback and registered_filters == filters
|
|
26
|
+
for registered_callback, registered_filters in self._callbacks
|
|
27
|
+
)
|
|
28
|
+
if not exists:
|
|
29
|
+
self._callbacks.append((callback, dict(filters)))
|
|
30
|
+
return callback
|
|
31
|
+
|
|
32
|
+
def unsubscribe(self, callback: ObserverCallback) -> None:
|
|
33
|
+
with self._lock:
|
|
34
|
+
self._callbacks = [
|
|
35
|
+
(registered_callback, registered_filters)
|
|
36
|
+
for registered_callback, registered_filters in self._callbacks
|
|
37
|
+
if registered_callback is not callback
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def callback(self, **filters: Any):
|
|
41
|
+
self._validate_filters(filters)
|
|
42
|
+
|
|
43
|
+
def decorator(fn: ObserverCallback) -> ObserverCallback:
|
|
44
|
+
return self.subscribe(fn, **filters)
|
|
45
|
+
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
def emit(self, ctx: ObserverContext) -> None:
|
|
49
|
+
with self._lock:
|
|
50
|
+
callbacks = tuple(self._callbacks)
|
|
51
|
+
|
|
52
|
+
for callback, filters in callbacks:
|
|
53
|
+
if not self._match_filters(ctx, filters):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
callback(ctx)
|
|
58
|
+
except Exception:
|
|
59
|
+
# 不要让监听器异常影响原始业务调用
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def observe(
|
|
63
|
+
self,
|
|
64
|
+
*,
|
|
65
|
+
include_private: bool = False,
|
|
66
|
+
emit_before: bool = True,
|
|
67
|
+
):
|
|
68
|
+
from .deractor import observe_methods
|
|
69
|
+
|
|
70
|
+
return observe_methods(
|
|
71
|
+
self,
|
|
72
|
+
include_private=include_private,
|
|
73
|
+
emit_before=emit_before,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _match_filters(self, ctx: ObserverContext, filters: dict[str, Any]) -> bool:
|
|
77
|
+
for key, expected in filters.items():
|
|
78
|
+
if getattr(ctx, key) != expected:
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def _validate_filters(self, filters: dict[str, Any]) -> None:
|
|
83
|
+
valid_keys = set(ObserverContext.__dataclass_fields__.keys())
|
|
84
|
+
invalid_keys = [key for key in filters if key not in valid_keys]
|
|
85
|
+
if invalid_keys:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"invalid callback filters: {', '.join(sorted(invalid_keys))}"
|
|
88
|
+
)
|
observer/context.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ObserverPhase = Literal["before", "after", "error"]
|
|
8
|
+
ObserverKind = Literal["instance", "class", "static"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class ObserverContext:
|
|
13
|
+
call_id: str
|
|
14
|
+
instance: Any | None
|
|
15
|
+
owner: Any | None
|
|
16
|
+
cls: type
|
|
17
|
+
cls_name: str
|
|
18
|
+
method_name: str
|
|
19
|
+
qualname: str
|
|
20
|
+
method_kind: ObserverKind
|
|
21
|
+
args: tuple[Any, ...]
|
|
22
|
+
kwargs: dict[str, Any]
|
|
23
|
+
result: Any = None
|
|
24
|
+
error: BaseException | None = None
|
|
25
|
+
phase: ObserverPhase = "after"
|
|
26
|
+
is_async: bool = False
|
|
27
|
+
started_at: float = 0.0
|
|
28
|
+
ended_at: float = 0.0
|
|
29
|
+
elapsed: float = 0.0
|
observer/deractor.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .bus import ObserverBus
|
|
11
|
+
from .context import ObserverContext, ObserverKind, ObserverPhase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_WRAPPED_FLAG = "__observer_bus_wrapped__"
|
|
15
|
+
_SUBCLASS_HOOK_FLAG = "__observer_bus_subclass_hook_installed__"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def observe_methods(
|
|
19
|
+
bus: ObserverBus,
|
|
20
|
+
*,
|
|
21
|
+
include_private: bool = False,
|
|
22
|
+
emit_before: bool = True,
|
|
23
|
+
):
|
|
24
|
+
def decorator(cls: type) -> type:
|
|
25
|
+
_observe_class(
|
|
26
|
+
cls,
|
|
27
|
+
bus=bus,
|
|
28
|
+
include_private=include_private,
|
|
29
|
+
emit_before=emit_before,
|
|
30
|
+
)
|
|
31
|
+
_install_subclass_hook(
|
|
32
|
+
cls,
|
|
33
|
+
bus=bus,
|
|
34
|
+
include_private=include_private,
|
|
35
|
+
emit_before=emit_before,
|
|
36
|
+
)
|
|
37
|
+
return cls
|
|
38
|
+
|
|
39
|
+
return decorator
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _install_subclass_hook(
|
|
43
|
+
cls: type,
|
|
44
|
+
*,
|
|
45
|
+
bus: ObserverBus,
|
|
46
|
+
include_private: bool,
|
|
47
|
+
emit_before: bool,
|
|
48
|
+
) -> None:
|
|
49
|
+
if cls.__dict__.get(_SUBCLASS_HOOK_FLAG, False):
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
original_init_subclass = cls.__dict__.get("__init_subclass__")
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def __init_subclass__(subcls, **kwargs):
|
|
56
|
+
if original_init_subclass is not None:
|
|
57
|
+
original_init_subclass.__get__(subcls, subcls)(**kwargs)
|
|
58
|
+
else:
|
|
59
|
+
super(cls, subcls).__init_subclass__(**kwargs)
|
|
60
|
+
|
|
61
|
+
_observe_class(
|
|
62
|
+
subcls,
|
|
63
|
+
bus=bus,
|
|
64
|
+
include_private=include_private,
|
|
65
|
+
emit_before=emit_before,
|
|
66
|
+
)
|
|
67
|
+
_install_subclass_hook(
|
|
68
|
+
subcls,
|
|
69
|
+
bus=bus,
|
|
70
|
+
include_private=include_private,
|
|
71
|
+
emit_before=emit_before,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
setattr(cls, "__init_subclass__", __init_subclass__)
|
|
75
|
+
setattr(cls, _SUBCLASS_HOOK_FLAG, True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _observe_class(
|
|
79
|
+
cls: type,
|
|
80
|
+
*,
|
|
81
|
+
bus: ObserverBus,
|
|
82
|
+
include_private: bool,
|
|
83
|
+
emit_before: bool,
|
|
84
|
+
) -> None:
|
|
85
|
+
for name, obj in list(cls.__dict__.items()):
|
|
86
|
+
if not include_private and name.startswith("_"):
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if isinstance(obj, property):
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if isinstance(obj, staticmethod):
|
|
93
|
+
func = obj.__func__
|
|
94
|
+
if getattr(func, _WRAPPED_FLAG, False):
|
|
95
|
+
continue
|
|
96
|
+
wrapped_func = _wrap_function(
|
|
97
|
+
owner_cls=cls,
|
|
98
|
+
method_name=name,
|
|
99
|
+
func=func,
|
|
100
|
+
bus=bus,
|
|
101
|
+
method_kind="static",
|
|
102
|
+
emit_before=emit_before,
|
|
103
|
+
)
|
|
104
|
+
setattr(cls, name, staticmethod(wrapped_func))
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if isinstance(obj, classmethod):
|
|
108
|
+
func = obj.__func__
|
|
109
|
+
if getattr(func, _WRAPPED_FLAG, False):
|
|
110
|
+
continue
|
|
111
|
+
wrapped_func = _wrap_function(
|
|
112
|
+
owner_cls=cls,
|
|
113
|
+
method_name=name,
|
|
114
|
+
func=func,
|
|
115
|
+
bus=bus,
|
|
116
|
+
method_kind="class",
|
|
117
|
+
emit_before=emit_before,
|
|
118
|
+
)
|
|
119
|
+
setattr(cls, name, classmethod(wrapped_func))
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if callable(obj):
|
|
123
|
+
if getattr(obj, _WRAPPED_FLAG, False):
|
|
124
|
+
continue
|
|
125
|
+
wrapped = _wrap_function(
|
|
126
|
+
owner_cls=cls,
|
|
127
|
+
method_name=name,
|
|
128
|
+
func=obj,
|
|
129
|
+
bus=bus,
|
|
130
|
+
method_kind="instance",
|
|
131
|
+
emit_before=emit_before,
|
|
132
|
+
)
|
|
133
|
+
setattr(cls, name, wrapped)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _wrap_function(
|
|
137
|
+
owner_cls: type,
|
|
138
|
+
method_name: str,
|
|
139
|
+
func: Callable[..., Any],
|
|
140
|
+
bus: ObserverBus,
|
|
141
|
+
method_kind: ObserverKind,
|
|
142
|
+
emit_before: bool,
|
|
143
|
+
):
|
|
144
|
+
if inspect.iscoroutinefunction(func):
|
|
145
|
+
@functools.wraps(func)
|
|
146
|
+
async def async_wrapper(*args, **kwargs):
|
|
147
|
+
call_id = uuid.uuid4().hex
|
|
148
|
+
started = time.perf_counter()
|
|
149
|
+
|
|
150
|
+
if emit_before:
|
|
151
|
+
bus.emit(
|
|
152
|
+
_build_context(
|
|
153
|
+
owner_cls=owner_cls,
|
|
154
|
+
method_name=method_name,
|
|
155
|
+
func=func,
|
|
156
|
+
method_kind=method_kind,
|
|
157
|
+
args=args,
|
|
158
|
+
kwargs=kwargs,
|
|
159
|
+
phase="before",
|
|
160
|
+
is_async=True,
|
|
161
|
+
started_at=started,
|
|
162
|
+
call_id=call_id,
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
result = await func(*args, **kwargs)
|
|
168
|
+
except Exception as error:
|
|
169
|
+
ended = time.perf_counter()
|
|
170
|
+
bus.emit(
|
|
171
|
+
_build_context(
|
|
172
|
+
owner_cls=owner_cls,
|
|
173
|
+
method_name=method_name,
|
|
174
|
+
func=func,
|
|
175
|
+
method_kind=method_kind,
|
|
176
|
+
args=args,
|
|
177
|
+
kwargs=kwargs,
|
|
178
|
+
phase="error",
|
|
179
|
+
is_async=True,
|
|
180
|
+
started_at=started,
|
|
181
|
+
ended_at=ended,
|
|
182
|
+
error=error,
|
|
183
|
+
call_id=call_id,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
ended = time.perf_counter()
|
|
189
|
+
bus.emit(
|
|
190
|
+
_build_context(
|
|
191
|
+
owner_cls=owner_cls,
|
|
192
|
+
method_name=method_name,
|
|
193
|
+
func=func,
|
|
194
|
+
method_kind=method_kind,
|
|
195
|
+
args=args,
|
|
196
|
+
kwargs=kwargs,
|
|
197
|
+
phase="after",
|
|
198
|
+
is_async=True,
|
|
199
|
+
started_at=started,
|
|
200
|
+
ended_at=ended,
|
|
201
|
+
result=result,
|
|
202
|
+
call_id=call_id,
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
setattr(async_wrapper, _WRAPPED_FLAG, True)
|
|
208
|
+
return async_wrapper
|
|
209
|
+
|
|
210
|
+
@functools.wraps(func)
|
|
211
|
+
def sync_wrapper(*args, **kwargs):
|
|
212
|
+
call_id = uuid.uuid4().hex
|
|
213
|
+
started = time.perf_counter()
|
|
214
|
+
|
|
215
|
+
if emit_before:
|
|
216
|
+
bus.emit(
|
|
217
|
+
_build_context(
|
|
218
|
+
owner_cls=owner_cls,
|
|
219
|
+
method_name=method_name,
|
|
220
|
+
func=func,
|
|
221
|
+
method_kind=method_kind,
|
|
222
|
+
args=args,
|
|
223
|
+
kwargs=kwargs,
|
|
224
|
+
phase="before",
|
|
225
|
+
is_async=False,
|
|
226
|
+
started_at=started,
|
|
227
|
+
call_id=call_id,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
result = func(*args, **kwargs)
|
|
233
|
+
except Exception as error:
|
|
234
|
+
ended = time.perf_counter()
|
|
235
|
+
bus.emit(
|
|
236
|
+
_build_context(
|
|
237
|
+
owner_cls=owner_cls,
|
|
238
|
+
method_name=method_name,
|
|
239
|
+
func=func,
|
|
240
|
+
method_kind=method_kind,
|
|
241
|
+
args=args,
|
|
242
|
+
kwargs=kwargs,
|
|
243
|
+
phase="error",
|
|
244
|
+
is_async=False,
|
|
245
|
+
started_at=started,
|
|
246
|
+
ended_at=ended,
|
|
247
|
+
error=error,
|
|
248
|
+
call_id=call_id,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
raise
|
|
252
|
+
|
|
253
|
+
ended = time.perf_counter()
|
|
254
|
+
bus.emit(
|
|
255
|
+
_build_context(
|
|
256
|
+
owner_cls=owner_cls,
|
|
257
|
+
method_name=method_name,
|
|
258
|
+
func=func,
|
|
259
|
+
method_kind=method_kind,
|
|
260
|
+
args=args,
|
|
261
|
+
kwargs=kwargs,
|
|
262
|
+
phase="after",
|
|
263
|
+
is_async=False,
|
|
264
|
+
started_at=started,
|
|
265
|
+
ended_at=ended,
|
|
266
|
+
result=result,
|
|
267
|
+
call_id=call_id,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
setattr(sync_wrapper, _WRAPPED_FLAG, True)
|
|
273
|
+
return sync_wrapper
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _build_context(
|
|
277
|
+
*,
|
|
278
|
+
owner_cls: type,
|
|
279
|
+
method_name: str,
|
|
280
|
+
func: Callable[..., Any],
|
|
281
|
+
method_kind: ObserverKind,
|
|
282
|
+
args: tuple[Any, ...],
|
|
283
|
+
kwargs: dict[str, Any],
|
|
284
|
+
phase: ObserverPhase,
|
|
285
|
+
is_async: bool,
|
|
286
|
+
started_at: float,
|
|
287
|
+
call_id: str,
|
|
288
|
+
ended_at: float | None = None,
|
|
289
|
+
result: Any = None,
|
|
290
|
+
error: BaseException | None = None,
|
|
291
|
+
) -> ObserverContext:
|
|
292
|
+
instance, owner, pure_args = _split_receiver(method_kind, args)
|
|
293
|
+
actual_cls = _resolve_context_class(
|
|
294
|
+
owner_cls=owner_cls,
|
|
295
|
+
method_kind=method_kind,
|
|
296
|
+
instance=instance,
|
|
297
|
+
owner=owner,
|
|
298
|
+
)
|
|
299
|
+
final_ended_at = ended_at if ended_at is not None else 0.0
|
|
300
|
+
elapsed = final_ended_at - started_at if ended_at is not None else 0.0
|
|
301
|
+
|
|
302
|
+
return ObserverContext(
|
|
303
|
+
call_id=call_id,
|
|
304
|
+
instance=instance,
|
|
305
|
+
owner=owner,
|
|
306
|
+
cls=actual_cls,
|
|
307
|
+
cls_name=actual_cls.__name__,
|
|
308
|
+
method_name=method_name,
|
|
309
|
+
qualname=func.__qualname__,
|
|
310
|
+
method_kind=method_kind,
|
|
311
|
+
args=pure_args,
|
|
312
|
+
kwargs=dict(kwargs),
|
|
313
|
+
result=result,
|
|
314
|
+
error=error,
|
|
315
|
+
phase=phase,
|
|
316
|
+
is_async=is_async,
|
|
317
|
+
started_at=started_at,
|
|
318
|
+
ended_at=final_ended_at,
|
|
319
|
+
elapsed=elapsed,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _split_receiver(
|
|
324
|
+
method_kind: ObserverKind,
|
|
325
|
+
args: tuple[Any, ...],
|
|
326
|
+
) -> tuple[Any | None, Any | None, tuple[Any, ...]]:
|
|
327
|
+
if method_kind == "instance":
|
|
328
|
+
instance = args[0] if args else None
|
|
329
|
+
pure_args = args[1:] if args else ()
|
|
330
|
+
return instance, instance, pure_args
|
|
331
|
+
|
|
332
|
+
if method_kind == "class":
|
|
333
|
+
owner = args[0] if args else None
|
|
334
|
+
pure_args = args[1:] if args else ()
|
|
335
|
+
return None, owner, pure_args
|
|
336
|
+
|
|
337
|
+
return None, None, args
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _resolve_context_class(
|
|
341
|
+
*,
|
|
342
|
+
owner_cls: type,
|
|
343
|
+
method_kind: ObserverKind,
|
|
344
|
+
instance: Any | None,
|
|
345
|
+
owner: Any | None,
|
|
346
|
+
) -> type:
|
|
347
|
+
if method_kind == "instance" and instance is not None:
|
|
348
|
+
return type(instance)
|
|
349
|
+
|
|
350
|
+
if method_kind == "class" and isinstance(owner, type):
|
|
351
|
+
return owner
|
|
352
|
+
|
|
353
|
+
return owner_cls
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
observer/__init__.py,sha256=BQHsdXKbTkLzwH_9Hols36mGACKQaiAN3JcwmbPT3xY,191
|
|
2
|
+
observer/bus.py,sha256=VqoHzuPPCPPdoT1NwTlTfXMxeaNsU68ZPOu1Y-jnvqA,2784
|
|
3
|
+
observer/context.py,sha256=-PmeYuT7wi-SlUjxTOZJMrETJIHsa_3aeEHJz8mzqyI,706
|
|
4
|
+
observer/deractor.py,sha256=maBrHvvHlMWb4LgWKzRgpgZKopyXDumV-bX0093Pfa4,10225
|
|
5
|
+
python_library_observer-0.1.0.dist-info/METADATA,sha256=pMKdq2oOlxgZYeQ3nURipN45QYou75AOIAJ06AZWBCA,91
|
|
6
|
+
python_library_observer-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
python_library_observer-0.1.0.dist-info/RECORD,,
|