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
@@ -0,0 +1,9 @@
1
+ from .bus import ObserverBus
2
+ from .context import ObserverContext
3
+ from .deractor import observe_methods
4
+
5
+ __all__ = [
6
+ "ObserverBus",
7
+ "ObserverContext",
8
+ "observe_methods",
9
+ ]
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,4 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-library-observer
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.10
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any