puffinflow 2.dev0__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.
Files changed (55) hide show
  1. puffinflow/__init__.py +132 -0
  2. puffinflow/core/__init__.py +110 -0
  3. puffinflow/core/agent/__init__.py +320 -0
  4. puffinflow/core/agent/base.py +1635 -0
  5. puffinflow/core/agent/checkpoint.py +50 -0
  6. puffinflow/core/agent/context.py +521 -0
  7. puffinflow/core/agent/decorators/__init__.py +90 -0
  8. puffinflow/core/agent/decorators/builder.py +454 -0
  9. puffinflow/core/agent/decorators/flexible.py +714 -0
  10. puffinflow/core/agent/decorators/inspection.py +144 -0
  11. puffinflow/core/agent/dependencies.py +57 -0
  12. puffinflow/core/agent/scheduling/__init__.py +21 -0
  13. puffinflow/core/agent/scheduling/builder.py +160 -0
  14. puffinflow/core/agent/scheduling/exceptions.py +35 -0
  15. puffinflow/core/agent/scheduling/inputs.py +137 -0
  16. puffinflow/core/agent/scheduling/parser.py +209 -0
  17. puffinflow/core/agent/scheduling/scheduler.py +413 -0
  18. puffinflow/core/agent/state.py +141 -0
  19. puffinflow/core/config.py +62 -0
  20. puffinflow/core/coordination/__init__.py +137 -0
  21. puffinflow/core/coordination/agent_group.py +359 -0
  22. puffinflow/core/coordination/agent_pool.py +629 -0
  23. puffinflow/core/coordination/agent_team.py +577 -0
  24. puffinflow/core/coordination/coordinator.py +720 -0
  25. puffinflow/core/coordination/deadlock.py +1759 -0
  26. puffinflow/core/coordination/fluent_api.py +421 -0
  27. puffinflow/core/coordination/primitives.py +478 -0
  28. puffinflow/core/coordination/rate_limiter.py +520 -0
  29. puffinflow/core/observability/__init__.py +47 -0
  30. puffinflow/core/observability/agent.py +139 -0
  31. puffinflow/core/observability/alerting.py +73 -0
  32. puffinflow/core/observability/config.py +127 -0
  33. puffinflow/core/observability/context.py +88 -0
  34. puffinflow/core/observability/core.py +147 -0
  35. puffinflow/core/observability/decorators.py +105 -0
  36. puffinflow/core/observability/events.py +71 -0
  37. puffinflow/core/observability/interfaces.py +196 -0
  38. puffinflow/core/observability/metrics.py +137 -0
  39. puffinflow/core/observability/tracing.py +209 -0
  40. puffinflow/core/reliability/__init__.py +27 -0
  41. puffinflow/core/reliability/bulkhead.py +96 -0
  42. puffinflow/core/reliability/circuit_breaker.py +149 -0
  43. puffinflow/core/reliability/leak_detector.py +122 -0
  44. puffinflow/core/resources/__init__.py +77 -0
  45. puffinflow/core/resources/allocation.py +790 -0
  46. puffinflow/core/resources/pool.py +645 -0
  47. puffinflow/core/resources/quotas.py +567 -0
  48. puffinflow/core/resources/requirements.py +217 -0
  49. puffinflow/version.py +21 -0
  50. puffinflow-2.dev0.dist-info/METADATA +334 -0
  51. puffinflow-2.dev0.dist-info/RECORD +55 -0
  52. puffinflow-2.dev0.dist-info/WHEEL +5 -0
  53. puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
  54. puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
  55. puffinflow-2.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from typing import Any, Optional
5
+
6
+ import aiohttp
7
+
8
+ from .config import AlertingConfig
9
+ from .interfaces import AlertingProvider, AlertSeverity
10
+
11
+
12
+ @dataclass
13
+ class Alert:
14
+ """Alert data structure"""
15
+
16
+ message: str
17
+ severity: AlertSeverity
18
+ attributes: dict[str, Any]
19
+ timestamp: Optional[datetime] = None
20
+
21
+ def __post_init__(self) -> None:
22
+ if self.timestamp is None:
23
+ self.timestamp = datetime.now()
24
+ else:
25
+ pass
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ return {
29
+ "message": self.message,
30
+ "severity": self.severity.value,
31
+ "attributes": self.attributes,
32
+ "timestamp": self.timestamp.isoformat() if self.timestamp else None,
33
+ }
34
+
35
+
36
+ class WebhookAlerting(AlertingProvider):
37
+ """Webhook-based alerting"""
38
+
39
+ def __init__(self, config: AlertingConfig):
40
+ self.config = config
41
+
42
+ async def send_alert(
43
+ self,
44
+ message: str,
45
+ severity: AlertSeverity,
46
+ attributes: Optional[dict[str, Any]] = None,
47
+ ) -> None:
48
+ """Send alert via webhooks"""
49
+ if not self.config.enabled or not self.config.webhook_urls:
50
+ return
51
+
52
+ alert = Alert(message, severity, attributes or {})
53
+ payload = {"alert": alert.to_dict()}
54
+
55
+ tasks = []
56
+ for webhook_url in self.config.webhook_urls:
57
+ task = asyncio.create_task(self._send_webhook(webhook_url, payload))
58
+ tasks.append(task)
59
+
60
+ if tasks:
61
+ await asyncio.gather(*tasks, return_exceptions=True)
62
+
63
+ async def _send_webhook(self, url: str, payload: dict[str, Any]) -> None:
64
+ """Send single webhook"""
65
+ try:
66
+ async with (
67
+ aiohttp.ClientSession() as session,
68
+ session.post(url, json=payload, timeout=30) as response,
69
+ ):
70
+ if response.status >= 400:
71
+ print(f"Webhook failed: {response.status}")
72
+ except Exception as e:
73
+ print(f"Failed to send webhook to {url}: {e}")
@@ -0,0 +1,127 @@
1
+ import os
2
+ from dataclasses import dataclass, field
3
+ from typing import Optional
4
+
5
+
6
+ @dataclass
7
+ class TracingConfig:
8
+ """Tracing configuration"""
9
+
10
+ enabled: bool = True
11
+ service_name: str = "puffinflow"
12
+ service_version: str = "1.0.0"
13
+ sample_rate: float = 1.0
14
+ otlp_endpoint: Optional[str] = None
15
+ jaeger_endpoint: Optional[str] = None
16
+ console_enabled: bool = False
17
+
18
+ @classmethod
19
+ def from_env(cls) -> "TracingConfig":
20
+ return cls(
21
+ enabled=os.getenv("TRACING_ENABLED", "true").lower() == "true",
22
+ service_name=os.getenv("SERVICE_NAME", "puffinflow"),
23
+ service_version=os.getenv("SERVICE_VERSION", "1.0.0"),
24
+ sample_rate=float(os.getenv("TRACE_SAMPLE_RATE", "1.0")),
25
+ otlp_endpoint=os.getenv("OTLP_ENDPOINT"),
26
+ jaeger_endpoint=os.getenv("JAEGER_ENDPOINT"),
27
+ console_enabled=os.getenv("TRACE_CONSOLE", "false").lower() == "true",
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class MetricsConfig:
33
+ """Metrics configuration"""
34
+
35
+ enabled: bool = True
36
+ namespace: str = "puffinflow"
37
+ prometheus_port: int = 9090
38
+ prometheus_path: str = "/metrics"
39
+ collection_interval: float = 15.0
40
+ cardinality_limit: int = 10000
41
+
42
+ @classmethod
43
+ def from_env(cls) -> "MetricsConfig":
44
+ return cls(
45
+ enabled=os.getenv("METRICS_ENABLED", "true").lower() == "true",
46
+ namespace=os.getenv("METRICS_NAMESPACE", "puffinflow"),
47
+ prometheus_port=int(os.getenv("METRICS_PORT", "9090")),
48
+ prometheus_path=os.getenv("METRICS_PATH", "/metrics"),
49
+ collection_interval=float(os.getenv("METRICS_INTERVAL", "15.0")),
50
+ cardinality_limit=int(os.getenv("METRICS_CARDINALITY_LIMIT", "10000")),
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class AlertingConfig:
56
+ """Alerting configuration"""
57
+
58
+ enabled: bool = True
59
+ evaluation_interval: float = 30.0
60
+ webhook_urls: list[str] = field(default_factory=list)
61
+ email_recipients: list[str] = field(default_factory=list)
62
+ slack_webhook_url: Optional[str] = None
63
+
64
+ @classmethod
65
+ def from_env(cls) -> "AlertingConfig":
66
+ webhook_urls = (
67
+ os.getenv("ALERT_WEBHOOK_URLS", "").split(",")
68
+ if os.getenv("ALERT_WEBHOOK_URLS")
69
+ else []
70
+ )
71
+ email_recipients = (
72
+ os.getenv("ALERT_EMAIL_RECIPIENTS", "").split(",")
73
+ if os.getenv("ALERT_EMAIL_RECIPIENTS")
74
+ else []
75
+ )
76
+
77
+ return cls(
78
+ enabled=os.getenv("ALERTING_ENABLED", "true").lower() == "true",
79
+ evaluation_interval=float(os.getenv("ALERT_EVALUATION_INTERVAL", "30.0")),
80
+ webhook_urls=[url.strip() for url in webhook_urls if url.strip()],
81
+ email_recipients=[
82
+ email.strip() for email in email_recipients if email.strip()
83
+ ],
84
+ slack_webhook_url=os.getenv("ALERT_SLACK_WEBHOOK"),
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class EventsConfig:
90
+ """Events configuration"""
91
+
92
+ enabled: bool = True
93
+ buffer_size: int = 1000
94
+ batch_size: int = 100
95
+ flush_interval: float = 5.0
96
+
97
+ @classmethod
98
+ def from_env(cls) -> "EventsConfig":
99
+ return cls(
100
+ enabled=os.getenv("EVENTS_ENABLED", "true").lower() == "true",
101
+ buffer_size=int(os.getenv("EVENT_BUFFER_SIZE", "1000")),
102
+ batch_size=int(os.getenv("EVENT_BATCH_SIZE", "100")),
103
+ flush_interval=float(os.getenv("EVENT_FLUSH_INTERVAL", "5.0")),
104
+ )
105
+
106
+
107
+ @dataclass
108
+ class ObservabilityConfig:
109
+ """Complete observability configuration"""
110
+
111
+ enabled: bool = True
112
+ environment: str = "development"
113
+ tracing: TracingConfig = field(default_factory=TracingConfig)
114
+ metrics: MetricsConfig = field(default_factory=MetricsConfig)
115
+ alerting: AlertingConfig = field(default_factory=AlertingConfig)
116
+ events: EventsConfig = field(default_factory=EventsConfig)
117
+
118
+ @classmethod
119
+ def from_env(cls) -> "ObservabilityConfig":
120
+ return cls(
121
+ enabled=os.getenv("OBSERVABILITY_ENABLED", "true").lower() == "true",
122
+ environment=os.getenv("ENVIRONMENT", "development"),
123
+ tracing=TracingConfig.from_env(),
124
+ metrics=MetricsConfig.from_env(),
125
+ alerting=AlertingConfig.from_env(),
126
+ events=EventsConfig.from_env(),
127
+ )
@@ -0,0 +1,88 @@
1
+ import time
2
+ from collections.abc import Generator
3
+ from contextlib import contextmanager
4
+ from datetime import datetime
5
+ from typing import Any, Optional
6
+
7
+ from ..agent.context import Context
8
+ from .core import ObservabilityManager
9
+ from .interfaces import ObservabilityEvent, SpanType
10
+
11
+
12
+ class ObservableContext(Context):
13
+ """Context with observability integration"""
14
+
15
+ def __init__(
16
+ self,
17
+ shared_state: dict[str, Any],
18
+ observability: Optional[ObservabilityManager] = None,
19
+ ):
20
+ super().__init__(shared_state)
21
+ self._observability = observability
22
+
23
+ @contextmanager
24
+ def trace(self, name: str, **attributes: Any) -> Generator[Any, None, None]:
25
+ """Create trace span with context"""
26
+ if not self._observability or not self._observability.tracing:
27
+ yield None
28
+ return
29
+
30
+ # Add context attributes
31
+ context_attrs = {
32
+ "workflow_id": self.get_variable("workflow_id"),
33
+ "agent_name": self.get_variable("agent_name"),
34
+ "state_name": self.get_variable("current_state"),
35
+ **attributes,
36
+ }
37
+
38
+ with self._observability.tracing.span(
39
+ name, SpanType.BUSINESS, **context_attrs
40
+ ) as span:
41
+ yield span
42
+
43
+ def metric(self, name: str, value: float, **labels: Any) -> None:
44
+ """Record metric with context"""
45
+ if not self._observability or not self._observability.metrics:
46
+ return
47
+
48
+ context_labels = {
49
+ "workflow_id": self.get_variable("workflow_id", "unknown"),
50
+ "agent_name": self.get_variable("agent_name", "unknown"),
51
+ **labels,
52
+ }
53
+
54
+ histogram = self._observability.histogram(
55
+ name, labels=list(context_labels.keys())
56
+ )
57
+ if histogram:
58
+ histogram.record(value, **context_labels)
59
+
60
+ def log(self, level: str, message: str, **kwargs: Any) -> None:
61
+ """Log with observability"""
62
+ if not self._observability or not self._observability.events:
63
+ return
64
+
65
+ event = ObservabilityEvent(
66
+ timestamp=datetime.fromtimestamp(time.time()),
67
+ event_type="log",
68
+ source="context",
69
+ level=level.upper(),
70
+ message=message,
71
+ attributes={
72
+ "workflow_id": self.get_variable("workflow_id"),
73
+ "agent_name": self.get_variable("agent_name"),
74
+ "state_name": self.get_variable("current_state"),
75
+ **kwargs,
76
+ },
77
+ )
78
+
79
+ import asyncio
80
+
81
+ try:
82
+ loop = asyncio.get_event_loop()
83
+ # Create task but don't store reference as it's fire-and-forget
84
+ task = loop.create_task(self._observability.events.process_event(event))
85
+ if task:
86
+ task.add_done_callback(lambda t: None) # Prevent warnings
87
+ except RuntimeError:
88
+ pass
@@ -0,0 +1,147 @@
1
+ import threading
2
+ from collections.abc import Iterator
3
+ from contextlib import contextmanager
4
+ from typing import Any, Optional
5
+
6
+ from .alerting import WebhookAlerting
7
+ from .config import ObservabilityConfig
8
+ from .events import BufferedEventProcessor
9
+ from .interfaces import (
10
+ AlertingProvider,
11
+ EventProcessor,
12
+ MetricsProvider,
13
+ TracingProvider,
14
+ )
15
+ from .metrics import PrometheusMetricsProvider
16
+ from .tracing import OpenTelemetryTracingProvider
17
+
18
+
19
+ class ObservabilityManager:
20
+ """Main observability coordinator"""
21
+
22
+ def __init__(self, config: Optional[ObservabilityConfig] = None):
23
+ self.config = config or ObservabilityConfig()
24
+ self._initialized = False
25
+ self._lock = threading.Lock()
26
+
27
+ # Initialize providers
28
+ self._tracing: Optional[TracingProvider] = None
29
+ self._metrics: Optional[MetricsProvider] = None
30
+ self._alerting: Optional[AlertingProvider] = None
31
+ self._events: Optional[EventProcessor] = None
32
+
33
+ @property
34
+ def tracing(self) -> Optional[TracingProvider]:
35
+ return self._tracing
36
+
37
+ @property
38
+ def metrics(self) -> Optional[MetricsProvider]:
39
+ return self._metrics
40
+
41
+ @property
42
+ def alerting(self) -> Optional[AlertingProvider]:
43
+ return self._alerting
44
+
45
+ @property
46
+ def events(self) -> Optional[EventProcessor]:
47
+ return self._events
48
+
49
+ async def initialize(self) -> None:
50
+ """Initialize all components"""
51
+ with self._lock:
52
+ if self._initialized:
53
+ return
54
+
55
+ # Initialize tracing
56
+ if self.config.tracing.enabled:
57
+ self._tracing = OpenTelemetryTracingProvider(self.config.tracing)
58
+
59
+ # Initialize metrics
60
+ if self.config.metrics.enabled:
61
+ self._metrics = PrometheusMetricsProvider(self.config.metrics)
62
+
63
+ # Initialize alerting
64
+ if self.config.alerting.enabled:
65
+ self._alerting = WebhookAlerting(self.config.alerting)
66
+
67
+ # Initialize events
68
+ if self.config.events.enabled:
69
+ self._events = BufferedEventProcessor(self.config.events)
70
+ await self._events.initialize()
71
+
72
+ self._initialized = True
73
+
74
+ async def shutdown(self) -> None:
75
+ """Shutdown all components"""
76
+ if self._events and hasattr(self._events, "shutdown"):
77
+ await self._events.shutdown()
78
+ self._initialized = False
79
+
80
+ # Convenience methods
81
+ @contextmanager
82
+ def trace(self, name: str, **attributes: Any) -> Iterator[Any]:
83
+ """Create trace span"""
84
+ if self._tracing and hasattr(self._tracing, "span"):
85
+ with self._tracing.span(name, **attributes) as span:
86
+ yield span
87
+ else:
88
+ yield None
89
+
90
+ def counter(
91
+ self, name: str, description: str = "", labels: Optional[list[str]] = None
92
+ ) -> Any:
93
+ """Create counter metric"""
94
+ if self._metrics:
95
+ return self._metrics.counter(name, description, labels)
96
+ return None
97
+
98
+ def gauge(
99
+ self, name: str, description: str = "", labels: Optional[list[str]] = None
100
+ ) -> Any:
101
+ """Create gauge metric"""
102
+ if self._metrics:
103
+ return self._metrics.gauge(name, description, labels)
104
+ return None
105
+
106
+ def histogram(
107
+ self, name: str, description: str = "", labels: Optional[list[str]] = None
108
+ ) -> Any:
109
+ """Create histogram metric"""
110
+ if self._metrics:
111
+ return self._metrics.histogram(name, description, labels)
112
+ return None
113
+
114
+ async def alert(
115
+ self, message: str, severity: str = "warning", **attributes: Any
116
+ ) -> None:
117
+ """Send alert"""
118
+ if self._alerting:
119
+ from .interfaces import AlertSeverity
120
+
121
+ sev = AlertSeverity(severity.lower())
122
+ await self._alerting.send_alert(message, sev, attributes)
123
+
124
+
125
+ # Global instance management
126
+ _global_observability: Optional[ObservabilityManager] = None
127
+ _lock = threading.Lock()
128
+
129
+
130
+ def get_observability() -> ObservabilityManager:
131
+ """Get global observability instance"""
132
+ global _global_observability
133
+ with _lock:
134
+ if _global_observability is None:
135
+ _global_observability = ObservabilityManager()
136
+ return _global_observability
137
+
138
+
139
+ async def setup_observability(
140
+ config: Optional[ObservabilityConfig] = None,
141
+ ) -> ObservabilityManager:
142
+ """Setup and initialize observability"""
143
+ global _global_observability
144
+ with _lock:
145
+ _global_observability = ObservabilityManager(config)
146
+ await _global_observability.initialize()
147
+ return _global_observability
@@ -0,0 +1,105 @@
1
+ import asyncio
2
+ import functools
3
+ import time
4
+ from typing import Any, Callable, Optional
5
+
6
+ from .core import get_observability
7
+ from .interfaces import SpanType
8
+
9
+
10
+ def observe(
11
+ name: Optional[str] = None,
12
+ span_type: SpanType = SpanType.BUSINESS,
13
+ **span_attributes: Any,
14
+ ) -> Callable[[Callable], Callable]:
15
+ """Observability decorator"""
16
+
17
+ def decorator(func: Callable) -> Callable:
18
+ @functools.wraps(func)
19
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
20
+ observability = get_observability()
21
+ operation_name = name or f"{func.__module__}.{func.__name__}"
22
+
23
+ if observability.tracing and hasattr(observability.tracing, "span"):
24
+ with observability.tracing.span(
25
+ operation_name, span_type, function=func.__name__, **span_attributes
26
+ ) as span:
27
+ start_time = time.time()
28
+ try:
29
+ result = await func(*args, **kwargs)
30
+ duration = time.time() - start_time
31
+
32
+ if span:
33
+ span.set_attribute("function.duration", duration)
34
+ span.set_status("ok")
35
+
36
+ return result
37
+ except Exception as e:
38
+ if span:
39
+ span.record_exception(e)
40
+ raise
41
+ else:
42
+ return await func(*args, **kwargs)
43
+
44
+ @functools.wraps(func)
45
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
46
+ observability = get_observability()
47
+ operation_name = name or f"{func.__module__}.{func.__name__}"
48
+
49
+ if observability.tracing and hasattr(observability.tracing, "span"):
50
+ with observability.tracing.span(
51
+ operation_name, span_type, **span_attributes
52
+ ) as span:
53
+ try:
54
+ result = func(*args, **kwargs)
55
+ if span:
56
+ span.set_status("ok")
57
+ return result
58
+ except Exception as e:
59
+ if span:
60
+ span.record_exception(e)
61
+ raise
62
+ else:
63
+ return func(*args, **kwargs)
64
+
65
+ return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
66
+
67
+ return decorator
68
+
69
+
70
+ def trace_state(
71
+ span_type: SpanType = SpanType.STATE, **span_attributes: Any
72
+ ) -> Callable[[Callable], Callable]:
73
+ """Decorator for tracing PuffinFlow states"""
74
+
75
+ def decorator(func: Callable) -> Callable:
76
+ @functools.wraps(func)
77
+ async def wrapper(context: Any, *args: Any, **kwargs: Any) -> Any:
78
+ observability = get_observability()
79
+
80
+ if observability.tracing and hasattr(observability.tracing, "span"):
81
+ attrs = {
82
+ "state.name": func.__name__,
83
+ "workflow.id": context.get_variable("workflow_id"),
84
+ "agent.name": context.get_variable("agent_name"),
85
+ **span_attributes,
86
+ }
87
+
88
+ with observability.tracing.span(
89
+ f"state.{func.__name__}", span_type, **attrs
90
+ ) as span:
91
+ try:
92
+ result = await func(context, *args, **kwargs)
93
+ if span:
94
+ span.set_status("ok")
95
+ return result
96
+ except Exception as e:
97
+ if span:
98
+ span.record_exception(e)
99
+ raise
100
+ else:
101
+ return await func(context, *args, **kwargs)
102
+
103
+ return wrapper
104
+
105
+ return decorator
@@ -0,0 +1,71 @@
1
+ import asyncio
2
+ import contextlib
3
+ from collections import deque
4
+ from typing import Any, Callable, Optional
5
+
6
+ from .config import EventsConfig
7
+ from .interfaces import EventProcessor, ObservabilityEvent
8
+
9
+
10
+ class BufferedEventProcessor(EventProcessor):
11
+ """Buffered event processor"""
12
+
13
+ def __init__(self, config: EventsConfig):
14
+ self.config = config
15
+ self.buffer: Any = deque(maxlen=config.buffer_size)
16
+ self.subscribers: list[Callable[[ObservabilityEvent], None]] = []
17
+ self._task: Optional[Any] = None
18
+ self._shutdown = False
19
+
20
+ async def initialize(self) -> None:
21
+ """Initialize event processor"""
22
+ if self.config.enabled:
23
+ self._task = asyncio.create_task(self._process_loop())
24
+
25
+ async def shutdown(self) -> None:
26
+ """Shutdown event processor"""
27
+ self._shutdown = True
28
+ if self._task:
29
+ self._task.cancel()
30
+ with contextlib.suppress(asyncio.CancelledError):
31
+ await self._task
32
+
33
+ async def process_event(self, event: ObservabilityEvent) -> None:
34
+ """Add event to buffer"""
35
+ if self.config.enabled:
36
+ self.buffer.append(event)
37
+
38
+ def subscribe(self, callback: Callable[[ObservabilityEvent], None]) -> None:
39
+ """Subscribe to events"""
40
+ self.subscribers.append(callback)
41
+
42
+ async def _process_loop(self) -> None:
43
+ """Process events from buffer"""
44
+ while not self._shutdown:
45
+ try:
46
+ events_to_process = []
47
+
48
+ # Collect batch
49
+ for _ in range(min(self.config.batch_size, len(self.buffer))):
50
+ if self.buffer:
51
+ events_to_process.append(self.buffer.popleft())
52
+
53
+ # Process events
54
+ for event in events_to_process:
55
+ for subscriber in self.subscribers:
56
+ try:
57
+ if asyncio.iscoroutinefunction(subscriber):
58
+ await subscriber(event)
59
+ else:
60
+ subscriber(event)
61
+ except Exception as e:
62
+ print(f"Event processing error: {e}")
63
+
64
+ # Wait for next batch
65
+ await asyncio.sleep(self.config.flush_interval)
66
+
67
+ except asyncio.CancelledError:
68
+ break
69
+ except Exception as e:
70
+ print(f"Event processing loop error: {e}")
71
+ await asyncio.sleep(1)