violet-poolController-api 0.0.1__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.
@@ -0,0 +1,235 @@
1
+ """Rate Limiter für API-Requests - Token Bucket Algorithm."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from collections import deque
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class RateLimiter:
14
+ """
15
+ Rate Limiter mit Token Bucket Algorithm.
16
+
17
+ Verhindert API-Overload durch:
18
+ - Maximale Requests pro Zeitfenster
19
+ - Burst-Support für kurzzeitige Spitzen
20
+ - Priority Queue für kritische Requests
21
+ - Graceful Degradation bei Limit-Überschreitung
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ max_requests: int = 10,
27
+ time_window: float = 1.0,
28
+ burst_size: int = 3,
29
+ retry_after: float = 0.1,
30
+ ) -> None:
31
+ """
32
+ Initialisiere den Rate Limiter.
33
+
34
+ Args:
35
+ max_requests: Maximale Anzahl Requests pro Zeitfenster
36
+ time_window: Zeitfenster in Sekunden
37
+ burst_size: Erlaubte Burst-Größe (zusätzliche Requests)
38
+ retry_after: Wartezeit in Sekunden bei Limit-Überschreitung
39
+ """
40
+ self.max_requests = max_requests
41
+ self.time_window = time_window
42
+ self.burst_size = burst_size
43
+ self.retry_after = retry_after
44
+
45
+ # Token Bucket
46
+ self.tokens = float(max_requests + burst_size)
47
+ self.max_tokens = max_requests + burst_size
48
+ self.last_refill = time.monotonic()
49
+
50
+ # Optimized request history with size and time limits
51
+ self.request_history: deque = deque(maxlen=500) # Reduced from 1000
52
+ self.blocked_requests = 0
53
+ self.total_requests = 0
54
+ self.history_cleanup_interval = 300 # 5 minutes
55
+ self.last_cleanup_time = time.monotonic()
56
+
57
+ # Memory-efficient statistics
58
+ self._recent_stats = {
59
+ "requests_last_minute": 0,
60
+ "blocked_last_minute": 0,
61
+ "last_minute_reset": time.monotonic(),
62
+ }
63
+
64
+ # Lock für Thread-Safety
65
+ self._lock = asyncio.Lock()
66
+
67
+ _LOGGER.debug(
68
+ "Rate Limiter initialisiert: %d req/%ss (burst: %d)",
69
+ max_requests,
70
+ time_window,
71
+ burst_size,
72
+ )
73
+
74
+ async def acquire(self, priority: int = 3) -> bool:
75
+ """
76
+ Acquire a token from the rate limiter.
77
+
78
+ Args:
79
+ priority: Priority level (0=highest, 3=lowest)
80
+
81
+ Returns:
82
+ True if token acquired, False otherwise
83
+ """
84
+ async with self._lock:
85
+ current_time = time.monotonic()
86
+ self.total_requests += 1
87
+
88
+ # Periodic cleanup to prevent memory growth
89
+ if current_time - self.last_cleanup_time > self.history_cleanup_interval:
90
+ await self._cleanup_history(current_time)
91
+ self.last_cleanup_time = current_time
92
+
93
+ # Update recent statistics
94
+ self._update_recent_stats(current_time)
95
+
96
+ # Refill tokens
97
+ await self._refill_tokens(current_time)
98
+
99
+ if self.tokens >= 1:
100
+ self.tokens -= 1
101
+
102
+ # Store minimal data for efficiency
103
+ self.request_history.append(
104
+ {"time": current_time, "priority": priority, "blocked": False}
105
+ )
106
+
107
+ return True
108
+
109
+ # Track failures efficiently
110
+ self.blocked_requests += 1
111
+ self._recent_stats["blocked_last_minute"] += 1
112
+ return False
113
+
114
+ async def _cleanup_history(self, current_time: float) -> None:
115
+ """Clean up old history entries to prevent memory leaks."""
116
+ # Remove entries older than 1 hour
117
+ cutoff_time = current_time - 3600
118
+
119
+ # Filter while maintaining order
120
+ filtered_history = deque(
121
+ (entry for entry in self.request_history if entry["time"] > cutoff_time),
122
+ maxlen=500,
123
+ )
124
+
125
+ self.request_history = filtered_history
126
+ _LOGGER.debug("Rate limiter history cleanup completed")
127
+
128
+ def _update_recent_stats(self, current_time: float) -> None:
129
+ """Update memory-efficient recent statistics."""
130
+ # Reset minute stats every 60 seconds
131
+ if current_time - self._recent_stats["last_minute_reset"] > 60:
132
+ self._recent_stats["requests_last_minute"] = 0
133
+ self._recent_stats["blocked_last_minute"] = 0
134
+ self._recent_stats["last_minute_reset"] = current_time
135
+
136
+ self._recent_stats["requests_last_minute"] += 1
137
+
138
+ async def wait_if_needed(self, priority: int = 3, timeout: float = 10.0) -> None:
139
+ """
140
+ Warte bis ein Token verfügbar ist.
141
+
142
+ Args:
143
+ priority: Request-Priorität
144
+ timeout: Maximale Wartezeit in Sekunden
145
+
146
+ Raises:
147
+ asyncio.TimeoutError: Wenn Timeout erreicht
148
+ """
149
+ start_time = time.monotonic()
150
+
151
+ while True:
152
+ if await self.acquire(priority):
153
+ return
154
+
155
+ # Timeout-Prüfung
156
+ elapsed = time.monotonic() - start_time
157
+ if elapsed >= timeout:
158
+ raise asyncio.TimeoutError(f"Rate Limiter timeout nach {elapsed:.1f}s")
159
+
160
+ # Warte auf Token-Refill
161
+ await asyncio.sleep(self.retry_after)
162
+
163
+ async def _refill_tokens(self, current_time: float) -> None:
164
+ """Fülle Token-Bucket basierend auf verstrichener Zeit."""
165
+ time_passed = current_time - self.last_refill
166
+
167
+ # Refill proportional to time passed, not just when full window elapsed
168
+ if time_passed > 0:
169
+ # Berechne neue Tokens basierend auf verstrichener Zeit
170
+ refill_rate = self.max_requests / self.time_window
171
+ new_tokens = time_passed * refill_rate
172
+
173
+ self.tokens = min(self.max_tokens, self.tokens + new_tokens)
174
+ self.last_refill = current_time
175
+
176
+ def get_stats(self) -> dict:
177
+ """Hole Rate-Limiter-Statistiken."""
178
+ current_time = time.monotonic()
179
+
180
+ # Berechne Requests in letzter Minute
181
+ recent_requests = [
182
+ r for r in self.request_history if current_time - r["time"] <= 60
183
+ ]
184
+ recent_blocked = sum(1 for r in recent_requests if r["blocked"])
185
+
186
+ return {
187
+ "total_requests": self.total_requests,
188
+ "blocked_requests": self.blocked_requests,
189
+ "recent_requests_1min": len(recent_requests),
190
+ "recent_blocked_1min": recent_blocked,
191
+ "current_tokens": self.tokens,
192
+ "max_tokens": self.max_tokens,
193
+ "block_rate": (
194
+ self.blocked_requests / self.total_requests * 100
195
+ if self.total_requests > 0
196
+ else 0
197
+ ),
198
+ }
199
+
200
+ def reset(self) -> None:
201
+ """Setze Rate Limiter zurück."""
202
+ self.tokens = float(self.max_tokens)
203
+ self.last_refill = time.monotonic()
204
+ self.blocked_requests = 0
205
+ self.total_requests = 0
206
+ self.request_history.clear()
207
+ _LOGGER.debug("Rate Limiter zurückgesetzt")
208
+
209
+
210
+ # Global Rate Limiter-Instanz (kann pro API-Instanz auch separat erstellt werden)
211
+ _global_rate_limiter: RateLimiter | None = None
212
+
213
+
214
+ def get_global_rate_limiter() -> RateLimiter:
215
+ """Hole oder erstelle globalen Rate Limiter."""
216
+ global _global_rate_limiter
217
+ if _global_rate_limiter is None:
218
+ # Default-Werte aus const_api
219
+ from .const_api import (
220
+ API_RATE_LIMIT_BURST,
221
+ API_RATE_LIMIT_REQUESTS,
222
+ API_RATE_LIMIT_RETRY_AFTER,
223
+ API_RATE_LIMIT_WINDOW,
224
+ )
225
+
226
+ _global_rate_limiter = RateLimiter(
227
+ max_requests=API_RATE_LIMIT_REQUESTS,
228
+ time_window=API_RATE_LIMIT_WINDOW,
229
+ burst_size=API_RATE_LIMIT_BURST,
230
+ retry_after=API_RATE_LIMIT_RETRY_AFTER,
231
+ )
232
+ return _global_rate_limiter
233
+
234
+
235
+ __all__ = ["RateLimiter", "get_global_rate_limiter"]