firefeed-core 1.0.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 (92) hide show
  1. firefeed_core/__init__.py +52 -0
  2. firefeed_core/api_client/__init__.py +17 -0
  3. firefeed_core/api_client/circuit_breaker.py +141 -0
  4. firefeed_core/api_client/client.py +434 -0
  5. firefeed_core/api_client/rate_limiter.py +269 -0
  6. firefeed_core/api_client/retry.py +183 -0
  7. firefeed_core/auth/__init__.py +16 -0
  8. firefeed_core/auth/permissions.py +243 -0
  9. firefeed_core/auth/token_manager.py +390 -0
  10. firefeed_core/auth/token_validator.py +241 -0
  11. firefeed_core/config/__init__.py +30 -0
  12. firefeed_core/config/base_config.py +375 -0
  13. firefeed_core/config/logging_config.py +21 -0
  14. firefeed_core/config/redis_config.py +210 -0
  15. firefeed_core/config/redis_utils.py +224 -0
  16. firefeed_core/config/services_config.py +251 -0
  17. firefeed_core/config/settings.py +139 -0
  18. firefeed_core/config/validation.py +414 -0
  19. firefeed_core/di_container.py +108 -0
  20. firefeed_core/email_service/__init__.py +0 -0
  21. firefeed_core/email_service/sender.py +825 -0
  22. firefeed_core/exceptions/__init__.py +161 -0
  23. firefeed_core/exceptions/api_exceptions.py +234 -0
  24. firefeed_core/exceptions/base_exceptions.py +139 -0
  25. firefeed_core/exceptions/database_exceptions.py +25 -0
  26. firefeed_core/exceptions/rss_exceptions.py +59 -0
  27. firefeed_core/exceptions/service_exceptions.py +236 -0
  28. firefeed_core/interfaces/__init__.py +11 -0
  29. firefeed_core/interfaces/base_interfaces.py +225 -0
  30. firefeed_core/interfaces/core_interfaces.py +65 -0
  31. firefeed_core/interfaces/email_interfaces.py +36 -0
  32. firefeed_core/interfaces/media_interfaces.py +36 -0
  33. firefeed_core/interfaces/repository_interfaces.py +212 -0
  34. firefeed_core/interfaces/rss_interfaces.py +171 -0
  35. firefeed_core/interfaces/telegram_interfaces.py +208 -0
  36. firefeed_core/interfaces/text_analysis_interfaces.py +36 -0
  37. firefeed_core/interfaces/translation_interfaces.py +116 -0
  38. firefeed_core/interfaces/user_interfaces.py +207 -0
  39. firefeed_core/models/__init__.py +11 -0
  40. firefeed_core/models/api_key_models.py +37 -0
  41. firefeed_core/models/base_models.py +1172 -0
  42. firefeed_core/models/category_models.py +8 -0
  43. firefeed_core/models/error_models.py +7 -0
  44. firefeed_core/models/media_models.py +7 -0
  45. firefeed_core/models/rss_models.py +96 -0
  46. firefeed_core/models/source_models.py +9 -0
  47. firefeed_core/models/telegram_models.py +18 -0
  48. firefeed_core/models/translation_models.py +14 -0
  49. firefeed_core/models/user_models.py +126 -0
  50. firefeed_core/services/email_service.py +825 -0
  51. firefeed_core/services/rss_service.py +185 -0
  52. firefeed_core/services/translation_service.py +602 -0
  53. firefeed_core/tests/__init__.py +3 -0
  54. firefeed_core/tests/integration/__init__.py +0 -0
  55. firefeed_core/tests/integration/test_api_client.py +283 -0
  56. firefeed_core/tests/integration/test_email_service.py +275 -0
  57. firefeed_core/tests/unit/__init__.py +0 -0
  58. firefeed_core/tests/unit/api/__init__.py +0 -0
  59. firefeed_core/tests/unit/config/__init__.py +0 -0
  60. firefeed_core/tests/unit/core/__init__.py +0 -0
  61. firefeed_core/tests/unit/database/__init__.py +0 -0
  62. firefeed_core/tests/unit/email/__init__.py +1 -0
  63. firefeed_core/tests/unit/email/test_email_sender.py +296 -0
  64. firefeed_core/tests/unit/exceptions/__init__.py +0 -0
  65. firefeed_core/tests/unit/models/__init__.py +0 -0
  66. firefeed_core/tests/unit/repositories/__init__.py +0 -0
  67. firefeed_core/tests/unit/services/__init__.py +0 -0
  68. firefeed_core/tests/unit/utils/__init__.py +0 -0
  69. firefeed_core/utils/__init__.py +355 -0
  70. firefeed_core/utils/api.py +67 -0
  71. firefeed_core/utils/async_utils.py +415 -0
  72. firefeed_core/utils/cache.py +74 -0
  73. firefeed_core/utils/cache_utils.py +404 -0
  74. firefeed_core/utils/cleanup.py +80 -0
  75. firefeed_core/utils/file_utils.py +421 -0
  76. firefeed_core/utils/formatting_utils.py +397 -0
  77. firefeed_core/utils/image.py +185 -0
  78. firefeed_core/utils/image_utils.py +333 -0
  79. firefeed_core/utils/network_utils.py +430 -0
  80. firefeed_core/utils/retry.py +72 -0
  81. firefeed_core/utils/retry_utils.py +350 -0
  82. firefeed_core/utils/security_utils.py +492 -0
  83. firefeed_core/utils/text.py +90 -0
  84. firefeed_core/utils/text_utils.py +431 -0
  85. firefeed_core/utils/time_utils.py +363 -0
  86. firefeed_core/utils/validation_utils.py +457 -0
  87. firefeed_core/utils/video.py +126 -0
  88. firefeed_core-1.0.0.dist-info/METADATA +276 -0
  89. firefeed_core-1.0.0.dist-info/RECORD +92 -0
  90. firefeed_core-1.0.0.dist-info/WHEEL +5 -0
  91. firefeed_core-1.0.0.dist-info/licenses/LICENSE +45 -0
  92. firefeed_core-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,52 @@
1
+ """
2
+ FireFeed Core - Core utilities and models for FireFeed microservices
3
+
4
+ This package contains shared components used across all FireFeed microservices:
5
+ - Common Pydantic models
6
+ - Exceptions and error handling
7
+ - API clients for inter-service communication
8
+ - Authentication and authorization utilities
9
+ - Configuration management
10
+ - Interface definitions
11
+ """
12
+
13
+ from . import models
14
+ from . import exceptions
15
+ from . import config
16
+ from . import auth
17
+ from . import api_client
18
+ from . import interfaces
19
+ from . import utils
20
+
21
+ # Version
22
+ __version__ = "1.0.0"
23
+
24
+ # Main exports
25
+ from .api_client import APIClient
26
+ from .auth.token_manager import ServiceTokenManager
27
+ from .config.settings import FireFeedSettings
28
+ from .exceptions import FireFeedException, APIException, AuthenticationException
29
+
30
+ __all__ = [
31
+ # Version
32
+ "__version__",
33
+
34
+ # Core components
35
+ "APIClient",
36
+ "ServiceTokenManager",
37
+ "FireFeedSettings",
38
+
39
+ # Exceptions
40
+ "FireFeedException",
41
+ "APIException",
42
+ "AuthenticationException",
43
+
44
+ # Submodules
45
+ "models",
46
+ "exceptions",
47
+ "config",
48
+ "auth",
49
+ "api_client",
50
+ "interfaces",
51
+ "utils",
52
+ ]
@@ -0,0 +1,17 @@
1
+ """
2
+ FireFeed Core API Client
3
+
4
+ Provides HTTP client functionality for inter-service communication.
5
+ """
6
+
7
+ from .client import APIClient
8
+ from .circuit_breaker import CircuitBreaker
9
+ from .retry import RetryPolicy
10
+ from .rate_limiter import RateLimiter
11
+
12
+ __all__ = [
13
+ "APIClient",
14
+ "CircuitBreaker",
15
+ "RetryPolicy",
16
+ "RateLimiter",
17
+ ]
@@ -0,0 +1,141 @@
1
+ """
2
+ Circuit Breaker implementation for FireFeed Core
3
+
4
+ Provides fault tolerance by preventing requests to failing services.
5
+ """
6
+
7
+ import time
8
+ from enum import Enum
9
+ from typing import Dict, Any
10
+
11
+
12
+ class CircuitState(Enum):
13
+ """Circuit breaker states."""
14
+ CLOSED = "closed" # Normal operation
15
+ OPEN = "open" # Blocking requests
16
+ HALF_OPEN = "half_open" # Testing if service recovered
17
+
18
+
19
+ class CircuitBreaker:
20
+ """
21
+ Circuit breaker implementation to prevent cascading failures.
22
+
23
+ States:
24
+ - CLOSED: Normal operation, all requests pass through
25
+ - OPEN: Service is failing, block all requests
26
+ - HALF_OPEN: Allow limited requests to test recovery
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ failure_threshold: int = 5,
32
+ timeout: int = 60,
33
+ recovery_timeout: int = 30,
34
+ success_threshold: int = 3
35
+ ):
36
+ """
37
+ Initialize circuit breaker.
38
+
39
+ Args:
40
+ failure_threshold: Number of failures to open circuit
41
+ timeout: Time in seconds before trying to close circuit
42
+ recovery_timeout: Time in half-open state before returning to closed
43
+ success_threshold: Number of successes needed to close circuit from half-open
44
+ """
45
+ self.failure_threshold = failure_threshold
46
+ self.timeout = timeout
47
+ self.recovery_timeout = recovery_timeout
48
+ self.success_threshold = success_threshold
49
+
50
+ self.state = CircuitState.CLOSED
51
+ self.failure_count = 0
52
+ self.success_count = 0
53
+ self.last_failure_time = None
54
+ self.last_success_time = None
55
+ self.last_state_change = time.time()
56
+
57
+ def allow_request(self) -> bool:
58
+ """
59
+ Check if request should be allowed based on circuit state.
60
+
61
+ Returns:
62
+ True if request should be allowed, False otherwise
63
+ """
64
+ current_time = time.time()
65
+
66
+ # State transitions
67
+ if self.state == CircuitState.OPEN:
68
+ # Check if timeout has passed, move to half-open
69
+ if current_time - self.last_state_change >= self.timeout:
70
+ self.state = CircuitState.HALF_OPEN
71
+ self.success_count = 0
72
+ self.last_state_change = current_time
73
+ return True
74
+ return False
75
+
76
+ elif self.state == CircuitState.HALF_OPEN:
77
+ # Allow request in half-open state
78
+ return True
79
+
80
+ else: # CLOSED
81
+ return True
82
+
83
+ def record_success(self):
84
+ """Record successful request."""
85
+ current_time = time.time()
86
+ self.last_success_time = current_time
87
+
88
+ if self.state == CircuitState.HALF_OPEN:
89
+ self.success_count += 1
90
+ if self.success_count >= self.success_threshold:
91
+ self.state = CircuitState.CLOSED
92
+ self.failure_count = 0
93
+ self.last_state_change = current_time
94
+ elif self.state == CircuitState.CLOSED:
95
+ # Reset failure count on success
96
+ self.failure_count = max(0, self.failure_count - 1)
97
+
98
+ def record_failure(self):
99
+ """Record failed request."""
100
+ current_time = time.time()
101
+ self.last_failure_time = current_time
102
+ self.failure_count += 1
103
+
104
+ if self.state == CircuitState.CLOSED:
105
+ if self.failure_count >= self.failure_threshold:
106
+ self.state = CircuitState.OPEN
107
+ self.last_state_change = current_time
108
+
109
+ elif self.state == CircuitState.HALF_OPEN:
110
+ # Return to open state on failure in half-open
111
+ self.state = CircuitState.OPEN
112
+ self.last_state_change = current_time
113
+
114
+ def get_stats(self) -> Dict[str, Any]:
115
+ """
116
+ Get circuit breaker statistics.
117
+
118
+ Returns:
119
+ Dictionary with circuit breaker stats
120
+ """
121
+ return {
122
+ "state": self.state.value,
123
+ "failure_count": self.failure_count,
124
+ "success_count": self.success_count,
125
+ "last_failure_time": self.last_failure_time,
126
+ "last_success_time": self.last_success_time,
127
+ "last_state_change": self.last_state_change,
128
+ "failure_threshold": self.failure_threshold,
129
+ "timeout": self.timeout,
130
+ "recovery_timeout": self.recovery_timeout,
131
+ "success_threshold": self.success_threshold,
132
+ }
133
+
134
+ def reset(self):
135
+ """Reset circuit breaker to initial state."""
136
+ self.state = CircuitState.CLOSED
137
+ self.failure_count = 0
138
+ self.success_count = 0
139
+ self.last_failure_time = None
140
+ self.last_success_time = None
141
+ self.last_state_change = time.time()
@@ -0,0 +1,434 @@
1
+ """
2
+ FireFeed Core API Client
3
+
4
+ A robust HTTP client for inter-service communication with authentication,
5
+ retry policies, circuit breaker pattern, and rate limiting.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ from typing import Any, Dict, List, Optional, Union
12
+ from urllib.parse import urljoin
13
+
14
+ import httpx
15
+ from pydantic import ValidationError as PydanticValidationError
16
+
17
+ from ..auth.token_manager import ServiceTokenManager
18
+ from ..exceptions.api_exceptions import (
19
+ APIException,
20
+ AuthenticationException,
21
+ AuthorizationException,
22
+ RateLimitException,
23
+ ServiceUnavailableException,
24
+ CircuitBreakerOpenException,
25
+ BadRequestException,
26
+ NotFoundException,
27
+ ConflictException,
28
+ )
29
+ from .circuit_breaker import CircuitBreaker
30
+ from .retry import RetryPolicy
31
+ from .rate_limiter import RateLimiter
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class APIClient:
38
+ """
39
+ HTTP client for FireFeed microservices communication.
40
+
41
+ Features:
42
+ - JWT token authentication
43
+ - Automatic retry with exponential backoff
44
+ - Circuit breaker pattern for fault tolerance
45
+ - Rate limiting to prevent abuse
46
+ - Request/response logging
47
+ - Timeout management
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ base_url: str,
53
+ token: str,
54
+ service_id: str,
55
+ timeout: int = 30,
56
+ max_retries: int = 3,
57
+ circuit_breaker_failure_threshold: int = 5,
58
+ circuit_breaker_timeout: int = 60,
59
+ rate_limit_requests: int = 100,
60
+ rate_limit_window: int = 60,
61
+ **httpx_kwargs
62
+ ):
63
+ """
64
+ Initialize API Client.
65
+
66
+ Args:
67
+ base_url: Base URL for the API (e.g., http://firefeed-api:8000)
68
+ token: JWT token for authentication
69
+ service_id: Unique identifier for this service
70
+ timeout: Request timeout in seconds
71
+ max_retries: Maximum number of retry attempts
72
+ circuit_breaker_failure_threshold: Number of failures to open circuit
73
+ circuit_breaker_timeout: Time in seconds before trying again
74
+ rate_limit_requests: Maximum requests per window
75
+ rate_limit_window: Time window in seconds for rate limiting
76
+ **httpx_kwargs: Additional arguments for httpx.AsyncClient
77
+ """
78
+ self.base_url = base_url.rstrip('/')
79
+ self.token = token
80
+ self.service_id = service_id
81
+ self.timeout = timeout
82
+
83
+ # Initialize components
84
+ self.token_manager = ServiceTokenManager(secret_key="", issuer="") # Will be set by validator
85
+ self.circuit_breaker = CircuitBreaker(
86
+ failure_threshold=circuit_breaker_failure_threshold,
87
+ timeout=circuit_breaker_timeout
88
+ )
89
+ self.retry_policy = RetryPolicy(max_retries=max_retries)
90
+ self.rate_limiter = RateLimiter(
91
+ max_requests=rate_limit_requests,
92
+ window_seconds=rate_limit_window
93
+ )
94
+
95
+ # HTTP client configuration
96
+ self.httpx_kwargs = {
97
+ "timeout": httpx.Timeout(timeout),
98
+ "headers": {
99
+ "User-Agent": f"firefeed-{service_id}/1.0.0",
100
+ "Content-Type": "application/json",
101
+ "Accept": "application/json",
102
+ },
103
+ **httpx_kwargs
104
+ }
105
+
106
+ # Create HTTP client
107
+ self.client = httpx.AsyncClient(**self.httpx_kwargs)
108
+
109
+ logger.info(f"Initialized APIClient for {service_id} -> {base_url}")
110
+
111
+ async def __aenter__(self):
112
+ """Async context manager entry."""
113
+ return self
114
+
115
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
116
+ """Async context manager exit."""
117
+ await self.close()
118
+
119
+ async def close(self):
120
+ """Close the HTTP client."""
121
+ await self.client.aclose()
122
+ logger.info("APIClient closed")
123
+
124
+ def _validate_token(self) -> str:
125
+ """
126
+ Validate and refresh token if needed.
127
+
128
+ Returns:
129
+ Valid JWT token string
130
+
131
+ Raises:
132
+ AuthenticationException: If token is invalid or expired
133
+ """
134
+ try:
135
+ # For now, assume token is valid if it exists
136
+ # In production, you would validate JWT signature and expiry
137
+ if not self.token:
138
+ raise AuthenticationException("No authentication token provided")
139
+
140
+ # TODO: Add JWT validation logic here
141
+ # decoded_token = self.token_manager.verify_token(self.token)
142
+ # Check expiry, issuer, etc.
143
+
144
+ return self.token
145
+
146
+ except Exception as e:
147
+ raise AuthenticationException(f"Token validation failed: {str(e)}")
148
+
149
+ def _get_headers(self) -> Dict[str, str]:
150
+ """
151
+ Get request headers including authentication.
152
+
153
+ Returns:
154
+ Dictionary of headers
155
+ """
156
+ token = self._validate_token()
157
+
158
+ headers = {
159
+ "Authorization": f"Bearer {token}",
160
+ "X-Service-ID": self.service_id,
161
+ "X-Request-ID": f"{self.service_id}-{int(time.time() * 1000000)}",
162
+ }
163
+
164
+ return headers
165
+
166
+ def _handle_response(self, response: httpx.Response) -> Any:
167
+ """
168
+ Handle HTTP response and convert to appropriate exceptions.
169
+
170
+ Args:
171
+ response: httpx.Response object
172
+
173
+ Returns:
174
+ Parsed JSON response data
175
+
176
+ Raises:
177
+ APIException: For various HTTP error codes
178
+ """
179
+ # Log response
180
+ logger.debug(
181
+ f"Response: {response.status_code} {response.request.url} "
182
+ f"({len(response.content)} bytes)"
183
+ )
184
+
185
+ try:
186
+ response_data = response.json() if response.content else {}
187
+ except Exception:
188
+ response_data = {"message": response.text}
189
+
190
+ # Handle HTTP status codes
191
+ if response.status_code == 200:
192
+ return response_data
193
+
194
+ elif response.status_code == 201:
195
+ return response_data
196
+
197
+ elif response.status_code == 400:
198
+ error_details = response_data.get("details", [])
199
+ raise BadRequestException(
200
+ message=response_data.get("error", {}).get("message", "Bad request"),
201
+ validation_errors=error_details
202
+ )
203
+
204
+ elif response.status_code == 401:
205
+ raise AuthenticationException(
206
+ message=response_data.get("error", {}).get("message", "Authentication failed")
207
+ )
208
+
209
+ elif response.status_code == 403:
210
+ raise AuthorizationException(
211
+ message=response_data.get("error", {}).get("message", "Forbidden")
212
+ )
213
+
214
+ elif response.status_code == 404:
215
+ raise NotFoundException(
216
+ message=response_data.get("error", {}).get("message", "Not found")
217
+ )
218
+
219
+ elif response.status_code == 409:
220
+ raise ConflictException(
221
+ message=response_data.get("error", {}).get("message", "Conflict")
222
+ )
223
+
224
+ elif response.status_code == 429:
225
+ retry_after = response.headers.get("Retry-After")
226
+ raise RateLimitException(
227
+ message=response_data.get("error", {}).get("message", "Rate limit exceeded"),
228
+ retry_after=int(retry_after) if retry_after else None
229
+ )
230
+
231
+ elif response.status_code >= 500:
232
+ raise ServiceUnavailableException(
233
+ message=response_data.get("error", {}).get("message", "Service unavailable")
234
+ )
235
+
236
+ else:
237
+ raise APIException(
238
+ message=f"Unexpected response: {response.status_code}",
239
+ status_code=response.status_code,
240
+ response_data=response_data
241
+ )
242
+
243
+ async def _make_request(
244
+ self,
245
+ method: str,
246
+ endpoint: str,
247
+ params: Optional[Dict[str, Any]] = None,
248
+ data: Optional[Dict[str, Any]] = None,
249
+ json_data: Optional[Dict[str, Any]] = None,
250
+ **kwargs
251
+ ) -> Any:
252
+ """
253
+ Make HTTP request with retry and circuit breaker logic.
254
+
255
+ Args:
256
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
257
+ endpoint: API endpoint (will be joined with base_url)
258
+ params: Query parameters
259
+ data: Form data
260
+ json_data: JSON data
261
+ **kwargs: Additional arguments for httpx request
262
+
263
+ Returns:
264
+ Parsed response data
265
+
266
+ Raises:
267
+ APIException: For various HTTP and network errors
268
+ """
269
+ url = urljoin(self.base_url + "/", endpoint.lstrip("/"))
270
+ headers = self._get_headers()
271
+
272
+ # Prepare request arguments
273
+ request_kwargs = {
274
+ "headers": headers,
275
+ "params": params or {},
276
+ **kwargs
277
+ }
278
+
279
+ if json_data is not None:
280
+ request_kwargs["json"] = json_data
281
+ elif data is not None:
282
+ request_kwargs["data"] = data
283
+
284
+ # Check rate limiting
285
+ if not self.rate_limiter.allow_request():
286
+ retry_after = self.rate_limiter.get_retry_after()
287
+ raise RateLimitException(
288
+ message="Rate limit exceeded",
289
+ retry_after=retry_after
290
+ )
291
+
292
+ # Check circuit breaker
293
+ if not self.circuit_breaker.allow_request():
294
+ raise CircuitBreakerOpenException(
295
+ message="Circuit breaker is open"
296
+ )
297
+
298
+ # Retry logic
299
+ last_exception = None
300
+ for attempt in range(self.retry_policy.max_retries + 1):
301
+ try:
302
+ logger.debug(f"Making request: {method} {url} (attempt {attempt + 1})")
303
+
304
+ response = await self.client.request(method, url, **request_kwargs)
305
+
306
+ # Record success for circuit breaker
307
+ self.circuit_breaker.record_success()
308
+
309
+ # Record rate limit usage
310
+ self.rate_limiter.record_request()
311
+
312
+ return self._handle_response(response)
313
+
314
+ except (httpx.TimeoutException, httpx.ConnectError, httpx.NetworkError) as e:
315
+ last_exception = e
316
+ self.circuit_breaker.record_failure()
317
+
318
+ if attempt < self.retry_policy.max_retries:
319
+ delay = self.retry_policy.get_delay(attempt)
320
+ logger.warning(
321
+ f"Request failed (attempt {attempt + 1}): {str(e)}. "
322
+ f"Retrying in {delay}s..."
323
+ )
324
+ await asyncio.sleep(delay)
325
+ continue
326
+ else:
327
+ break
328
+
329
+ except APIException as e:
330
+ # Don't retry on client errors (4xx)
331
+ if 400 <= e.status_code < 500:
332
+ self.circuit_breaker.record_failure()
333
+ raise
334
+ else:
335
+ # Retry on server errors (5xx)
336
+ last_exception = e
337
+ self.circuit_breaker.record_failure()
338
+
339
+ if attempt < self.retry_policy.max_retries:
340
+ delay = self.retry_policy.get_delay(attempt)
341
+ logger.warning(
342
+ f"Request failed (attempt {attempt + 1}): {str(e)}. "
343
+ f"Retrying in {delay}s..."
344
+ )
345
+ await asyncio.sleep(delay)
346
+ continue
347
+ else:
348
+ break
349
+
350
+ # All retries failed
351
+ if last_exception:
352
+ raise ServiceUnavailableException(
353
+ f"Request failed after {self.retry_policy.max_retries + 1} attempts: {str(last_exception)}"
354
+ )
355
+
356
+ # HTTP method wrappers
357
+
358
+ async def get(
359
+ self,
360
+ endpoint: str,
361
+ params: Optional[Dict[str, Any]] = None,
362
+ **kwargs
363
+ ) -> Any:
364
+ """Make GET request."""
365
+ return await self._make_request("GET", endpoint, params=params, **kwargs)
366
+
367
+ async def post(
368
+ self,
369
+ endpoint: str,
370
+ json_data: Optional[Dict[str, Any]] = None,
371
+ data: Optional[Dict[str, Any]] = None,
372
+ **kwargs
373
+ ) -> Any:
374
+ """Make POST request."""
375
+ return await self._make_request("POST", endpoint, json_data=json_data, data=data, **kwargs)
376
+
377
+ async def put(
378
+ self,
379
+ endpoint: str,
380
+ json_data: Optional[Dict[str, Any]] = None,
381
+ data: Optional[Dict[str, Any]] = None,
382
+ **kwargs
383
+ ) -> Any:
384
+ """Make PUT request."""
385
+ return await self._make_request("PUT", endpoint, json_data=json_data, data=data, **kwargs)
386
+
387
+ async def patch(
388
+ self,
389
+ endpoint: str,
390
+ json_data: Optional[Dict[str, Any]] = None,
391
+ data: Optional[Dict[str, Any]] = None,
392
+ **kwargs
393
+ ) -> Any:
394
+ """Make PATCH request."""
395
+ return await self._make_request("PATCH", endpoint, json_data=json_data, data=data, **kwargs)
396
+
397
+ async def delete(
398
+ self,
399
+ endpoint: str,
400
+ **kwargs
401
+ ) -> Any:
402
+ """Make DELETE request."""
403
+ return await self._make_request("DELETE", endpoint, **kwargs)
404
+
405
+ # Utility methods
406
+
407
+ async def health_check(self) -> bool:
408
+ """
409
+ Perform health check against the service.
410
+
411
+ Returns:
412
+ True if service is healthy, False otherwise
413
+ """
414
+ try:
415
+ response = await self.get("/health")
416
+ return response.get("status") == "healthy"
417
+ except Exception as e:
418
+ logger.warning(f"Health check failed: {str(e)}")
419
+ return False
420
+
421
+ def get_stats(self) -> Dict[str, Any]:
422
+ """
423
+ Get client statistics.
424
+
425
+ Returns:
426
+ Dictionary with client statistics
427
+ """
428
+ return {
429
+ "service_id": self.service_id,
430
+ "base_url": self.base_url,
431
+ "circuit_breaker": self.circuit_breaker.get_stats(),
432
+ "rate_limiter": self.rate_limiter.get_stats(),
433
+ "retry_policy": self.retry_policy.get_stats(),
434
+ }