mcp-security-framework 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.
Files changed (76) hide show
  1. mcp_security_framework/__init__.py +96 -0
  2. mcp_security_framework/cli/__init__.py +18 -0
  3. mcp_security_framework/cli/cert_cli.py +511 -0
  4. mcp_security_framework/cli/security_cli.py +791 -0
  5. mcp_security_framework/constants.py +209 -0
  6. mcp_security_framework/core/__init__.py +61 -0
  7. mcp_security_framework/core/auth_manager.py +1011 -0
  8. mcp_security_framework/core/cert_manager.py +1663 -0
  9. mcp_security_framework/core/permission_manager.py +735 -0
  10. mcp_security_framework/core/rate_limiter.py +602 -0
  11. mcp_security_framework/core/security_manager.py +943 -0
  12. mcp_security_framework/core/ssl_manager.py +735 -0
  13. mcp_security_framework/examples/__init__.py +75 -0
  14. mcp_security_framework/examples/django_example.py +615 -0
  15. mcp_security_framework/examples/fastapi_example.py +472 -0
  16. mcp_security_framework/examples/flask_example.py +506 -0
  17. mcp_security_framework/examples/gateway_example.py +803 -0
  18. mcp_security_framework/examples/microservice_example.py +690 -0
  19. mcp_security_framework/examples/standalone_example.py +576 -0
  20. mcp_security_framework/middleware/__init__.py +250 -0
  21. mcp_security_framework/middleware/auth_middleware.py +292 -0
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
  23. mcp_security_framework/middleware/fastapi_middleware.py +757 -0
  24. mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
  25. mcp_security_framework/middleware/flask_middleware.py +591 -0
  26. mcp_security_framework/middleware/mtls_middleware.py +439 -0
  27. mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
  28. mcp_security_framework/middleware/security_middleware.py +507 -0
  29. mcp_security_framework/schemas/__init__.py +109 -0
  30. mcp_security_framework/schemas/config.py +694 -0
  31. mcp_security_framework/schemas/models.py +709 -0
  32. mcp_security_framework/schemas/responses.py +686 -0
  33. mcp_security_framework/tests/__init__.py +0 -0
  34. mcp_security_framework/utils/__init__.py +121 -0
  35. mcp_security_framework/utils/cert_utils.py +525 -0
  36. mcp_security_framework/utils/crypto_utils.py +475 -0
  37. mcp_security_framework/utils/validation_utils.py +571 -0
  38. mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
  39. mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
  40. mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
  41. mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
  42. mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
  43. tests/__init__.py +0 -0
  44. tests/test_cli/__init__.py +0 -0
  45. tests/test_cli/test_cert_cli.py +379 -0
  46. tests/test_cli/test_security_cli.py +657 -0
  47. tests/test_core/__init__.py +0 -0
  48. tests/test_core/test_auth_manager.py +582 -0
  49. tests/test_core/test_cert_manager.py +795 -0
  50. tests/test_core/test_permission_manager.py +395 -0
  51. tests/test_core/test_rate_limiter.py +626 -0
  52. tests/test_core/test_security_manager.py +841 -0
  53. tests/test_core/test_ssl_manager.py +532 -0
  54. tests/test_examples/__init__.py +8 -0
  55. tests/test_examples/test_fastapi_example.py +264 -0
  56. tests/test_examples/test_flask_example.py +238 -0
  57. tests/test_examples/test_standalone_example.py +292 -0
  58. tests/test_integration/__init__.py +0 -0
  59. tests/test_integration/test_auth_flow.py +502 -0
  60. tests/test_integration/test_certificate_flow.py +527 -0
  61. tests/test_integration/test_fastapi_integration.py +341 -0
  62. tests/test_integration/test_flask_integration.py +398 -0
  63. tests/test_integration/test_standalone_integration.py +493 -0
  64. tests/test_middleware/__init__.py +0 -0
  65. tests/test_middleware/test_fastapi_middleware.py +523 -0
  66. tests/test_middleware/test_flask_middleware.py +582 -0
  67. tests/test_middleware/test_security_middleware.py +493 -0
  68. tests/test_schemas/__init__.py +0 -0
  69. tests/test_schemas/test_config.py +811 -0
  70. tests/test_schemas/test_models.py +879 -0
  71. tests/test_schemas/test_responses.py +1054 -0
  72. tests/test_schemas/test_serialization.py +493 -0
  73. tests/test_utils/__init__.py +0 -0
  74. tests/test_utils/test_cert_utils.py +510 -0
  75. tests/test_utils/test_crypto_utils.py +603 -0
  76. tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,602 @@
1
+ """
2
+ Rate Limiter Module
3
+
4
+ This module provides comprehensive rate limiting functionality for the
5
+ MCP Security Framework. It implements flexible rate limiting with support
6
+ for multiple identifiers, configurable windows, and various storage backends.
7
+
8
+ Key Features:
9
+ - Configurable rate limiting windows
10
+ - Support for multiple identifiers (IP, user, global)
11
+ - Burst limit support
12
+ - In-memory storage backend
13
+ - Cleanup of expired entries
14
+ - Rate limit status tracking
15
+
16
+ Classes:
17
+ RateLimiter: Main rate limiting class
18
+ RateLimitEntry: Internal rate limit entry
19
+ RateLimitStorage: Abstract storage interface
20
+
21
+ Author: MCP Security Team
22
+ Version: 1.0.0
23
+ License: MIT
24
+ """
25
+
26
+ import logging
27
+ import threading
28
+ import time
29
+ from collections import defaultdict
30
+ from datetime import datetime, timedelta, timezone
31
+ from typing import Dict, Optional, Set
32
+
33
+ from ..schemas.config import RateLimitConfig
34
+ from ..schemas.models import RateLimitStatus
35
+
36
+
37
+ class RateLimitEntry:
38
+ """
39
+ Rate Limit Entry Class
40
+
41
+ This class represents a single rate limit entry for tracking
42
+ requests within a specific time window.
43
+
44
+ Attributes:
45
+ identifier: Rate limiting identifier
46
+ count: Current request count
47
+ window_start: Start time of current window
48
+ window_size: Size of window in seconds
49
+ limit: Maximum allowed requests
50
+ """
51
+
52
+ def __init__(self, identifier: str, limit: int, window_size: int):
53
+ """
54
+ Initialize rate limit entry.
55
+
56
+ Args:
57
+ identifier: Rate limiting identifier
58
+ limit: Maximum allowed requests
59
+ window_size: Window size in seconds
60
+ """
61
+ self.identifier = identifier
62
+ self.count = 0
63
+ self.window_start = datetime.now(timezone.utc)
64
+ self.window_size = window_size
65
+ self.limit = limit
66
+
67
+ def is_expired(self) -> bool:
68
+ """
69
+ Check if the rate limit entry is expired.
70
+
71
+ Returns:
72
+ bool: True if entry is expired, False otherwise
73
+ """
74
+ now = datetime.now(timezone.utc)
75
+ return (now - self.window_start).total_seconds() >= self.window_size
76
+
77
+ def reset_window(self) -> None:
78
+ """Reset the rate limit window."""
79
+ self.count = 0
80
+ self.window_start = datetime.now(timezone.utc)
81
+
82
+ def increment(self) -> int:
83
+ """
84
+ Increment request count.
85
+
86
+ Returns:
87
+ int: New request count
88
+ """
89
+ self.count += 1
90
+ return self.count
91
+
92
+ def get_status(self) -> RateLimitStatus:
93
+ """
94
+ Get current rate limit status.
95
+
96
+ Returns:
97
+ RateLimitStatus: Current rate limit status
98
+ """
99
+ now = datetime.now(timezone.utc)
100
+ window_end = self.window_start.replace(tzinfo=timezone.utc) + timedelta(
101
+ seconds=self.window_size
102
+ )
103
+
104
+ is_exceeded = self.count > self.limit
105
+ remaining_requests = max(0, self.limit - self.count)
106
+
107
+ return RateLimitStatus(
108
+ identifier=self.identifier,
109
+ current_count=self.count,
110
+ limit=self.limit,
111
+ window_start=self.window_start,
112
+ window_end=window_end,
113
+ is_exceeded=is_exceeded,
114
+ remaining_requests=remaining_requests,
115
+ reset_time=window_end,
116
+ window_size_seconds=self.window_size,
117
+ )
118
+
119
+
120
+ class RateLimiter:
121
+ """
122
+ Rate Limiter Class
123
+
124
+ This class provides comprehensive rate limiting functionality with
125
+ support for multiple identifiers, configurable windows, and various
126
+ storage backends.
127
+
128
+ The RateLimiter implements:
129
+ - Configurable rate limiting windows
130
+ - Support for multiple identifiers (IP, user, global)
131
+ - Burst limit support
132
+ - In-memory storage backend
133
+ - Cleanup of expired entries
134
+ - Rate limit status tracking
135
+
136
+ Attributes:
137
+ config: Rate limiting configuration
138
+ logger: Logger instance for rate limiting operations
139
+ _entries: Dictionary of rate limit entries
140
+ _lock: Thread lock for thread safety
141
+ _cleanup_thread: Background cleanup thread
142
+ _stop_cleanup: Flag to stop cleanup thread
143
+ """
144
+
145
+ def __init__(self, config: RateLimitConfig):
146
+ """
147
+ Initialize Rate Limiter.
148
+
149
+ Args:
150
+ config: Rate limiting configuration containing limits,
151
+ window sizes, and storage settings. Must be a valid
152
+ RateLimitConfig instance with proper rate limiting
153
+ parameters.
154
+
155
+ Raises:
156
+ ValueError: If configuration is invalid
157
+
158
+ Example:
159
+ >>> config = RateLimitConfig(enabled=True, default_requests_per_minute=60)
160
+ >>> rate_limiter = RateLimiter(config)
161
+ """
162
+ self.config = config
163
+ self.logger = logging.getLogger(__name__)
164
+
165
+ # Initialize storage
166
+ self._entries: Dict[str, RateLimitEntry] = {}
167
+ self._lock = threading.RLock()
168
+
169
+ # Initialize cleanup thread
170
+ self._stop_cleanup = False
171
+ self._cleanup_thread = None
172
+
173
+ if self.config.enabled:
174
+ self._start_cleanup_thread()
175
+
176
+ self.logger.info(
177
+ "Rate limiter initialized",
178
+ extra={
179
+ "enabled": config.enabled,
180
+ "default_requests_per_minute": config.default_requests_per_minute,
181
+ "window_size_seconds": config.window_size_seconds,
182
+ "storage_backend": config.storage_backend,
183
+ },
184
+ )
185
+
186
+ def check_rate_limit(self, identifier: str, limit: Optional[int] = None) -> bool:
187
+ """
188
+ Check if rate limit is exceeded for the given identifier.
189
+
190
+ This method checks if the rate limit is exceeded for the specified
191
+ identifier. It automatically handles window resets and burst limits.
192
+
193
+ Args:
194
+ identifier: Rate limiting identifier (IP, user ID, etc.)
195
+ Must be a non-empty string identifying the request source.
196
+ limit: Custom limit for this check. If None, uses default
197
+ limit from configuration. Must be a positive integer.
198
+
199
+ Returns:
200
+ bool: True if rate limit is not exceeded, False if exceeded.
201
+ Returns True when the request can proceed, False when
202
+ rate limit is exceeded.
203
+
204
+ Raises:
205
+ ValueError: If identifier is empty or limit is invalid
206
+
207
+ Example:
208
+ >>> rate_limiter = RateLimiter(config)
209
+ >>> if rate_limiter.check_rate_limit("192.168.1.1"):
210
+ ... # Process request
211
+ ... pass
212
+ >>> else:
213
+ ... # Rate limit exceeded
214
+ ... pass
215
+ """
216
+ if not identifier:
217
+ raise ValueError("Identifier cannot be empty")
218
+
219
+ if limit is not None and limit <= 0:
220
+ raise ValueError("Limit must be a positive integer")
221
+
222
+ if not self.config.enabled:
223
+ return True
224
+
225
+ # Use default limit if not specified
226
+ if limit is None:
227
+ limit = self.config.default_requests_per_minute * self.config.burst_limit
228
+
229
+ with self._lock:
230
+ entry = self._get_or_create_entry(identifier, limit)
231
+
232
+ # Check if window needs to be reset
233
+ if entry.is_expired():
234
+ entry.reset_window()
235
+ self.logger.debug(
236
+ "Rate limit window reset",
237
+ extra={"identifier": identifier, "limit": limit},
238
+ )
239
+
240
+ # Check if rate limit is exceeded (including the current request)
241
+ is_exceeded = entry.count >= limit
242
+
243
+ if is_exceeded:
244
+ self.logger.warning(
245
+ "Rate limit exceeded",
246
+ extra={
247
+ "identifier": identifier,
248
+ "current_count": entry.count,
249
+ "limit": limit,
250
+ },
251
+ )
252
+ else:
253
+ self.logger.debug(
254
+ "Rate limit check passed",
255
+ extra={
256
+ "identifier": identifier,
257
+ "current_count": entry.count,
258
+ "limit": limit,
259
+ },
260
+ )
261
+
262
+ return not is_exceeded
263
+
264
+ def increment_request_count(
265
+ self, identifier: str, limit: Optional[int] = None
266
+ ) -> int:
267
+ """
268
+ Increment request count for the given identifier.
269
+
270
+ This method increments the request count for the specified identifier
271
+ and returns the new count. It automatically handles window resets.
272
+
273
+ Args:
274
+ identifier: Rate limiting identifier (IP, user ID, etc.)
275
+ Must be a non-empty string identifying the request source.
276
+ limit: Custom limit for this increment. If None, uses default
277
+ limit from configuration. Must be a positive integer.
278
+
279
+ Returns:
280
+ int: New request count after increment.
281
+ Returns the updated count for the current window.
282
+
283
+ Raises:
284
+ ValueError: If identifier is empty or limit is invalid
285
+
286
+ Example:
287
+ >>> rate_limiter = RateLimiter(config)
288
+ >>> new_count = rate_limiter.increment_request_count("192.168.1.1")
289
+ >>> print(f"New request count: {new_count}")
290
+ """
291
+ if not identifier:
292
+ raise ValueError("Identifier cannot be empty")
293
+
294
+ if limit is not None and limit <= 0:
295
+ raise ValueError("Limit must be a positive integer")
296
+
297
+ if not self.config.enabled:
298
+ return 0
299
+
300
+ # Use default limit if not specified
301
+ if limit is None:
302
+ limit = self.config.default_requests_per_minute * self.config.burst_limit
303
+
304
+ with self._lock:
305
+ entry = self._get_or_create_entry(identifier, limit)
306
+
307
+ # Check if window needs to be reset
308
+ if entry.is_expired():
309
+ entry.reset_window()
310
+ self.logger.debug(
311
+ "Rate limit window reset before increment",
312
+ extra={"identifier": identifier, "limit": limit},
313
+ )
314
+
315
+ # Increment count
316
+ new_count = entry.increment()
317
+
318
+ self.logger.debug(
319
+ "Request count incremented",
320
+ extra={
321
+ "identifier": identifier,
322
+ "new_count": new_count,
323
+ "limit": limit,
324
+ },
325
+ )
326
+
327
+ return new_count
328
+
329
+ def reset_rate_limit(self, identifier: str) -> None:
330
+ """
331
+ Reset rate limit for the given identifier.
332
+
333
+ This method completely resets the rate limit for the specified
334
+ identifier, clearing all request counts and starting a new window.
335
+
336
+ Args:
337
+ identifier: Rate limiting identifier (IP, user ID, etc.)
338
+ Must be a non-empty string identifying the request source.
339
+
340
+ Raises:
341
+ ValueError: If identifier is empty
342
+
343
+ Example:
344
+ >>> rate_limiter = RateLimiter(config)
345
+ >>> rate_limiter.reset_rate_limit("192.168.1.1")
346
+ """
347
+ if not identifier:
348
+ raise ValueError("Identifier cannot be empty")
349
+
350
+ with self._lock:
351
+ if identifier in self._entries:
352
+ del self._entries[identifier]
353
+ self.logger.info("Rate limit reset", extra={"identifier": identifier})
354
+
355
+ def get_rate_limit_status(
356
+ self, identifier: str, limit: Optional[int] = None
357
+ ) -> RateLimitStatus:
358
+ """
359
+ Get current rate limit status for the given identifier.
360
+
361
+ This method returns detailed information about the current rate
362
+ limit status for the specified identifier.
363
+
364
+ Args:
365
+ identifier: Rate limiting identifier (IP, user ID, etc.)
366
+ Must be a non-empty string identifying the request source.
367
+ limit: Custom limit for status check. If None, uses default
368
+ limit from configuration. Must be a positive integer.
369
+
370
+ Returns:
371
+ RateLimitStatus: Current rate limit status containing count,
372
+ limit, window information, and reset time.
373
+
374
+ Raises:
375
+ ValueError: If identifier is empty or limit is invalid
376
+
377
+ Example:
378
+ >>> rate_limiter = RateLimiter(config)
379
+ >>> status = rate_limiter.get_rate_limit_status("192.168.1.1")
380
+ >>> print(f"Current count: {status.current_count}")
381
+ >>> print(f"Remaining requests: {status.remaining_requests}")
382
+ """
383
+ if not identifier:
384
+ raise ValueError("Identifier cannot be empty")
385
+
386
+ if limit is not None and limit <= 0:
387
+ raise ValueError("Limit must be a positive integer")
388
+
389
+ if not self.config.enabled:
390
+ # Return default status when rate limiting is disabled
391
+ now = datetime.now(timezone.utc)
392
+ return RateLimitStatus(
393
+ identifier=identifier,
394
+ current_count=0,
395
+ limit=limit or self.config.default_requests_per_minute,
396
+ window_start=now,
397
+ window_end=now,
398
+ is_exceeded=False,
399
+ remaining_requests=limit or self.config.default_requests_per_minute,
400
+ reset_time=now,
401
+ window_size_seconds=self.config.window_size_seconds,
402
+ )
403
+
404
+ # Use default limit if not specified
405
+ if limit is None:
406
+ limit = self.config.default_requests_per_minute * self.config.burst_limit
407
+
408
+ with self._lock:
409
+ entry = self._get_or_create_entry(identifier, limit)
410
+
411
+ # Check if window needs to be reset
412
+ if entry.is_expired():
413
+ entry.reset_window()
414
+ self.logger.debug(
415
+ "Rate limit window reset during status check",
416
+ extra={"identifier": identifier, "limit": limit},
417
+ )
418
+
419
+ return entry.get_status()
420
+
421
+ def is_exempt(
422
+ self,
423
+ identifier: str,
424
+ path: Optional[str] = None,
425
+ roles: Optional[Set[str]] = None,
426
+ ) -> bool:
427
+ """
428
+ Check if the identifier is exempt from rate limiting.
429
+
430
+ This method checks if the identifier, path, or roles are exempt
431
+ from rate limiting based on configuration.
432
+
433
+ Args:
434
+ identifier: Rate limiting identifier (IP, user ID, etc.)
435
+ Must be a non-empty string identifying the request source.
436
+ path: Request path to check for exemption
437
+ Optional path to check against exempt_paths configuration.
438
+ roles: User roles to check for exemption
439
+ Optional set of roles to check against exempt_roles configuration.
440
+
441
+ Returns:
442
+ bool: True if exempt from rate limiting, False otherwise.
443
+ Returns True when the request should bypass rate limiting.
444
+
445
+ Example:
446
+ >>> rate_limiter = RateLimiter(config)
447
+ >>> is_exempt = rate_limiter.is_exempt("192.168.1.1", "/health")
448
+ >>> if is_exempt:
449
+ ... # Skip rate limiting
450
+ ... pass
451
+ """
452
+ if not identifier:
453
+ return False
454
+
455
+ # Check exempt paths
456
+ if path and path in self.config.exempt_paths:
457
+ self.logger.debug(
458
+ "Rate limit exempt due to path",
459
+ extra={"identifier": identifier, "path": path},
460
+ )
461
+ return True
462
+
463
+ # Check exempt roles
464
+ if roles and any(role in self.config.exempt_roles for role in roles):
465
+ self.logger.debug(
466
+ "Rate limit exempt due to role",
467
+ extra={"identifier": identifier, "roles": list(roles)},
468
+ )
469
+ return True
470
+
471
+ return False
472
+
473
+ def cleanup_expired_entries(self) -> int:
474
+ """
475
+ Clean up expired rate limit entries.
476
+
477
+ This method removes all expired rate limit entries to prevent
478
+ memory leaks and maintain performance.
479
+
480
+ Returns:
481
+ int: Number of entries cleaned up.
482
+ Returns the count of removed expired entries.
483
+
484
+ Example:
485
+ >>> rate_limiter = RateLimiter(config)
486
+ >>> cleaned_count = rate_limiter.cleanup_expired_entries()
487
+ >>> print(f"Cleaned up {cleaned_count} expired entries")
488
+ """
489
+ if not self.config.enabled:
490
+ return 0
491
+
492
+ with self._lock:
493
+ expired_identifiers = [
494
+ identifier
495
+ for identifier, entry in self._entries.items()
496
+ if entry.is_expired()
497
+ ]
498
+
499
+ for identifier in expired_identifiers:
500
+ del self._entries[identifier]
501
+
502
+ if expired_identifiers:
503
+ self.logger.info(
504
+ "Cleaned up expired rate limit entries",
505
+ extra={
506
+ "cleaned_count": len(expired_identifiers),
507
+ "remaining_entries": len(self._entries),
508
+ },
509
+ )
510
+
511
+ return len(expired_identifiers)
512
+
513
+ def get_statistics(self) -> Dict[str, any]:
514
+ """
515
+ Get rate limiter statistics.
516
+
517
+ This method returns comprehensive statistics about the rate limiter
518
+ including entry counts, memory usage, and performance metrics.
519
+
520
+ Returns:
521
+ Dict[str, any]: Rate limiter statistics containing entry counts,
522
+ memory usage, and performance information.
523
+
524
+ Example:
525
+ >>> rate_limiter = RateLimiter(config)
526
+ >>> stats = rate_limiter.get_statistics()
527
+ >>> print(f"Active entries: {stats['active_entries']}")
528
+ """
529
+ with self._lock:
530
+ active_entries = len(self._entries)
531
+ expired_entries = sum(
532
+ 1 for entry in self._entries.values() if entry.is_expired()
533
+ )
534
+
535
+ return {
536
+ "enabled": self.config.enabled,
537
+ "active_entries": active_entries,
538
+ "expired_entries": expired_entries,
539
+ "total_entries": active_entries + expired_entries,
540
+ "default_requests_per_minute": self.config.default_requests_per_minute,
541
+ "window_size_seconds": self.config.window_size_seconds,
542
+ "storage_backend": self.config.storage_backend,
543
+ "cleanup_interval": self.config.cleanup_interval,
544
+ }
545
+
546
+ def _get_or_create_entry(self, identifier: str, limit: int) -> RateLimitEntry:
547
+ """
548
+ Get or create a rate limit entry for the identifier.
549
+
550
+ Args:
551
+ identifier: Rate limiting identifier
552
+ limit: Request limit
553
+
554
+ Returns:
555
+ RateLimitEntry: Rate limit entry for the identifier
556
+ """
557
+ if identifier not in self._entries:
558
+ self._entries[identifier] = RateLimitEntry(
559
+ identifier, limit, self.config.window_size_seconds
560
+ )
561
+
562
+ return self._entries[identifier]
563
+
564
+ def _start_cleanup_thread(self) -> None:
565
+ """Start background cleanup thread."""
566
+ if self._cleanup_thread is not None:
567
+ return
568
+
569
+ self._stop_cleanup = False
570
+ self._cleanup_thread = threading.Thread(
571
+ target=self._cleanup_worker, daemon=True, name="RateLimiterCleanup"
572
+ )
573
+ self._cleanup_thread.start()
574
+
575
+ self.logger.info(
576
+ "Started rate limiter cleanup thread",
577
+ extra={"cleanup_interval": self.config.cleanup_interval},
578
+ )
579
+
580
+ def _cleanup_worker(self) -> None:
581
+ """Background cleanup worker thread."""
582
+ while not self._stop_cleanup:
583
+ try:
584
+ time.sleep(self.config.cleanup_interval)
585
+ if not self._stop_cleanup:
586
+ self.cleanup_expired_entries()
587
+ except Exception as e:
588
+ self.logger.error(
589
+ "Error in cleanup worker", extra={"error": str(e)}, exc_info=True
590
+ )
591
+
592
+ def stop_cleanup(self) -> None:
593
+ """Stop background cleanup thread."""
594
+ self._stop_cleanup = True
595
+ if self._cleanup_thread and self._cleanup_thread.is_alive():
596
+ self._cleanup_thread.join(timeout=5)
597
+
598
+ self.logger.info("Stopped rate limiter cleanup thread")
599
+
600
+ def __del__(self):
601
+ """Cleanup on destruction."""
602
+ self.stop_cleanup()