amazon-ads-mcp 0.2.7__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 (82) hide show
  1. amazon_ads_mcp/__init__.py +11 -0
  2. amazon_ads_mcp/auth/__init__.py +33 -0
  3. amazon_ads_mcp/auth/base.py +211 -0
  4. amazon_ads_mcp/auth/hooks.py +172 -0
  5. amazon_ads_mcp/auth/manager.py +791 -0
  6. amazon_ads_mcp/auth/oauth_state_store.py +277 -0
  7. amazon_ads_mcp/auth/providers/__init__.py +14 -0
  8. amazon_ads_mcp/auth/providers/direct.py +393 -0
  9. amazon_ads_mcp/auth/providers/example_auth0.py.example +216 -0
  10. amazon_ads_mcp/auth/providers/openbridge.py +512 -0
  11. amazon_ads_mcp/auth/registry.py +146 -0
  12. amazon_ads_mcp/auth/secure_token_store.py +297 -0
  13. amazon_ads_mcp/auth/token_store.py +723 -0
  14. amazon_ads_mcp/config/__init__.py +5 -0
  15. amazon_ads_mcp/config/sampling.py +111 -0
  16. amazon_ads_mcp/config/settings.py +366 -0
  17. amazon_ads_mcp/exceptions.py +314 -0
  18. amazon_ads_mcp/middleware/__init__.py +11 -0
  19. amazon_ads_mcp/middleware/authentication.py +1474 -0
  20. amazon_ads_mcp/middleware/caching.py +177 -0
  21. amazon_ads_mcp/middleware/oauth.py +175 -0
  22. amazon_ads_mcp/middleware/sampling.py +112 -0
  23. amazon_ads_mcp/models/__init__.py +320 -0
  24. amazon_ads_mcp/models/amc_models.py +837 -0
  25. amazon_ads_mcp/models/api_responses.py +847 -0
  26. amazon_ads_mcp/models/base_models.py +215 -0
  27. amazon_ads_mcp/models/builtin_responses.py +496 -0
  28. amazon_ads_mcp/models/dsp_models.py +556 -0
  29. amazon_ads_mcp/models/stores_brands.py +610 -0
  30. amazon_ads_mcp/server/__init__.py +6 -0
  31. amazon_ads_mcp/server/__main__.py +6 -0
  32. amazon_ads_mcp/server/builtin_prompts.py +269 -0
  33. amazon_ads_mcp/server/builtin_tools.py +962 -0
  34. amazon_ads_mcp/server/file_routes.py +547 -0
  35. amazon_ads_mcp/server/html_templates.py +149 -0
  36. amazon_ads_mcp/server/mcp_server.py +327 -0
  37. amazon_ads_mcp/server/openapi_utils.py +158 -0
  38. amazon_ads_mcp/server/sampling_handler.py +251 -0
  39. amazon_ads_mcp/server/server_builder.py +751 -0
  40. amazon_ads_mcp/server/sidecar_loader.py +178 -0
  41. amazon_ads_mcp/server/transform_executor.py +827 -0
  42. amazon_ads_mcp/tools/__init__.py +22 -0
  43. amazon_ads_mcp/tools/cache_management.py +105 -0
  44. amazon_ads_mcp/tools/download_tools.py +267 -0
  45. amazon_ads_mcp/tools/identity.py +236 -0
  46. amazon_ads_mcp/tools/oauth.py +598 -0
  47. amazon_ads_mcp/tools/profile.py +150 -0
  48. amazon_ads_mcp/tools/profile_listing.py +285 -0
  49. amazon_ads_mcp/tools/region.py +320 -0
  50. amazon_ads_mcp/tools/region_identity.py +175 -0
  51. amazon_ads_mcp/utils/__init__.py +6 -0
  52. amazon_ads_mcp/utils/async_compat.py +215 -0
  53. amazon_ads_mcp/utils/errors.py +452 -0
  54. amazon_ads_mcp/utils/export_content_type_resolver.py +249 -0
  55. amazon_ads_mcp/utils/export_download_handler.py +579 -0
  56. amazon_ads_mcp/utils/header_resolver.py +81 -0
  57. amazon_ads_mcp/utils/http/__init__.py +56 -0
  58. amazon_ads_mcp/utils/http/circuit_breaker.py +127 -0
  59. amazon_ads_mcp/utils/http/client_manager.py +329 -0
  60. amazon_ads_mcp/utils/http/request.py +207 -0
  61. amazon_ads_mcp/utils/http/resilience.py +512 -0
  62. amazon_ads_mcp/utils/http/resilient_client.py +195 -0
  63. amazon_ads_mcp/utils/http/retry.py +76 -0
  64. amazon_ads_mcp/utils/http_client.py +873 -0
  65. amazon_ads_mcp/utils/media/__init__.py +21 -0
  66. amazon_ads_mcp/utils/media/negotiator.py +243 -0
  67. amazon_ads_mcp/utils/media/types.py +199 -0
  68. amazon_ads_mcp/utils/openapi/__init__.py +16 -0
  69. amazon_ads_mcp/utils/openapi/json.py +55 -0
  70. amazon_ads_mcp/utils/openapi/loader.py +263 -0
  71. amazon_ads_mcp/utils/openapi/refs.py +46 -0
  72. amazon_ads_mcp/utils/region_config.py +200 -0
  73. amazon_ads_mcp/utils/response_wrapper.py +171 -0
  74. amazon_ads_mcp/utils/sampling_helpers.py +156 -0
  75. amazon_ads_mcp/utils/sampling_wrapper.py +173 -0
  76. amazon_ads_mcp/utils/security.py +630 -0
  77. amazon_ads_mcp/utils/tool_naming.py +137 -0
  78. amazon_ads_mcp-0.2.7.dist-info/METADATA +664 -0
  79. amazon_ads_mcp-0.2.7.dist-info/RECORD +82 -0
  80. amazon_ads_mcp-0.2.7.dist-info/WHEEL +4 -0
  81. amazon_ads_mcp-0.2.7.dist-info/entry_points.txt +3 -0
  82. amazon_ads_mcp-0.2.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,56 @@
1
+ """HTTP utilities public API (barrel module).
2
+
3
+ This package provides:
4
+ - Shared HTTP client manager and helpers
5
+ - Authenticated client for Amazon Ads API
6
+ - Retry decorator with jittered backoff
7
+ - Circuit breaker
8
+ - Request helpers and convenience wrappers
9
+
10
+ All names are re-exported here to preserve import compatibility.
11
+
12
+ Recommended import pattern for consumers:
13
+ from amazon_ads_mcp.utils.http import get_http_client, async_retry, make_request
14
+ from amazon_ads_mcp.utils.http import AuthenticatedClient # For direct use
15
+
16
+ This keeps call sites stable even if internal modules are reorganized.
17
+ """
18
+
19
+ from .circuit_breaker import CircuitBreaker, CircuitBreakerState
20
+ from .client_manager import (
21
+ HTTPClientManager,
22
+ create_limits,
23
+ create_timeout,
24
+ get_http_client,
25
+ health_check,
26
+ http_client_manager,
27
+ )
28
+ from .request import (
29
+ HTTPResponse,
30
+ delete,
31
+ get,
32
+ make_request,
33
+ patch,
34
+ post,
35
+ put,
36
+ )
37
+ from .retry import async_retry
38
+
39
+ __all__ = [
40
+ "HTTPClientManager",
41
+ "http_client_manager",
42
+ "get_http_client",
43
+ "create_timeout",
44
+ "create_limits",
45
+ "health_check",
46
+ "async_retry",
47
+ "CircuitBreaker",
48
+ "CircuitBreakerState",
49
+ "HTTPResponse",
50
+ "make_request",
51
+ "get",
52
+ "post",
53
+ "put",
54
+ "delete",
55
+ "patch",
56
+ ]
@@ -0,0 +1,127 @@
1
+ """Circuit breaker implementation for HTTP requests.
2
+
3
+ This module provides a circuit breaker pattern implementation designed
4
+ for HTTP operations. The circuit breaker helps prevent cascading failures
5
+ by temporarily stopping requests when a service is experiencing issues,
6
+ allowing it to recover before resuming normal operation.
7
+
8
+ The implementation supports configurable failure thresholds, recovery
9
+ timeouts, and exception types, making it suitable for various HTTP
10
+ client scenarios.
11
+ """
12
+
13
+ import asyncio
14
+ from functools import wraps
15
+ from typing import Any, Callable, Type
16
+
17
+ import httpx
18
+
19
+
20
+ class CircuitBreakerState:
21
+ """Constants representing the possible states of a circuit breaker.
22
+
23
+ The circuit breaker operates in three states:
24
+ - CLOSED: Normal operation, requests are allowed
25
+ - OPEN: Circuit is open, requests are blocked
26
+ - HALF_OPEN: Testing if service has recovered
27
+ """
28
+
29
+ CLOSED = "closed"
30
+ OPEN = "open"
31
+ HALF_OPEN = "half_open"
32
+
33
+
34
+ class CircuitBreaker:
35
+ """Circuit breaker decorator for async functions.
36
+
37
+ This class implements the circuit breaker pattern as a decorator
38
+ that can be applied to async functions. It monitors failures and
39
+ automatically opens the circuit when the failure threshold is
40
+ reached, preventing further requests until the recovery timeout
41
+ has elapsed.
42
+
43
+ The circuit breaker supports custom failure thresholds, recovery
44
+ timeouts, and exception types to handle different failure scenarios.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ failure_threshold: int = 5,
50
+ recovery_timeout: float = 60.0,
51
+ expected_exception: Type[Exception] = httpx.RequestError,
52
+ ):
53
+ """Initialize the circuit breaker with configuration.
54
+
55
+ :param failure_threshold: Number of consecutive failures before
56
+ opening the circuit
57
+ :type failure_threshold: int
58
+ :param recovery_timeout: Time in seconds to wait before attempting
59
+ to reset the circuit
60
+ :type recovery_timeout: float
61
+ :param expected_exception: Exception type to monitor for failures
62
+ :type expected_exception: Type[Exception]
63
+ """
64
+ self.failure_threshold = failure_threshold
65
+ self.recovery_timeout = recovery_timeout
66
+ self.expected_exception = expected_exception
67
+ self.failure_count = 0
68
+ self.last_failure_time = None
69
+ self.state = CircuitBreakerState.CLOSED
70
+
71
+ def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
72
+ """Apply the circuit breaker pattern to the decorated function.
73
+
74
+ :param func: The async function to wrap with circuit breaker logic
75
+ :type func: Callable[..., Any]
76
+ :return: Wrapped function with circuit breaker behavior
77
+ :rtype: Callable[..., Any]
78
+ """
79
+
80
+ @wraps(func)
81
+ async def wrapper(*args, **kwargs):
82
+ if self.state == CircuitBreakerState.OPEN:
83
+ if self._should_attempt_reset():
84
+ self.state = CircuitBreakerState.HALF_OPEN
85
+ else:
86
+ raise Exception("Circuit breaker is OPEN")
87
+ try:
88
+ result = await func(*args, **kwargs)
89
+ self._on_success()
90
+ return result
91
+ except self.expected_exception:
92
+ self._on_failure()
93
+ raise
94
+
95
+ return wrapper
96
+
97
+ def _should_attempt_reset(self) -> bool:
98
+ """Determine if enough time has passed to attempt resetting the circuit.
99
+
100
+ :return: True if recovery timeout has elapsed, False otherwise
101
+ :rtype: bool
102
+ """
103
+ return (
104
+ self.last_failure_time is not None
105
+ and asyncio.get_event_loop().time() - self.last_failure_time
106
+ >= self.recovery_timeout
107
+ )
108
+
109
+ def _on_success(self):
110
+ """Handle successful function execution.
111
+
112
+ Resets the failure count and closes the circuit, allowing
113
+ normal operation to resume.
114
+ """
115
+ self.failure_count = 0
116
+ self.state = CircuitBreakerState.CLOSED
117
+
118
+ def _on_failure(self):
119
+ """Handle function execution failure.
120
+
121
+ Increments the failure count and records the failure time.
122
+ Opens the circuit if the failure threshold is reached.
123
+ """
124
+ self.failure_count += 1
125
+ self.last_failure_time = asyncio.get_event_loop().time()
126
+ if self.failure_count >= self.failure_threshold:
127
+ self.state = CircuitBreakerState.OPEN
@@ -0,0 +1,329 @@
1
+ """HTTP client manager with connection pooling and lifecycle management.
2
+
3
+ This module provides a singleton HTTP client manager that handles
4
+ creation, caching, and lifecycle management of HTTP clients. It
5
+ implements connection pooling, configurable timeouts and limits,
6
+ and supports both HTTP/1.1 and HTTP/2 protocols.
7
+
8
+ The manager ensures efficient resource usage by reusing clients
9
+ with matching configurations and provides centralized cleanup
10
+ for all managed HTTP clients.
11
+ """
12
+
13
+ import asyncio
14
+ import logging
15
+ import os
16
+ from typing import Any, Dict, Optional, Type
17
+
18
+ import httpx
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class HTTPClientManager:
24
+ """Manages shared HTTP clients with connection pooling.
25
+
26
+ This singleton class manages the lifecycle of HTTP clients,
27
+ providing connection pooling, configurable timeouts and limits,
28
+ and automatic cleanup. It caches clients based on configuration
29
+ parameters to avoid creating duplicate clients with the same settings.
30
+
31
+ The manager supports both managed clients (created internally)
32
+ and external clients (registered for cleanup tracking).
33
+ """
34
+
35
+ _instance: Optional["HTTPClientManager"] = None
36
+ _lock = asyncio.Lock()
37
+ _external_clients: set = set()
38
+
39
+ def __new__(cls):
40
+ """Ensure singleton pattern - only one instance exists.
41
+
42
+ :return: The single instance of HTTPClientManager
43
+ :rtype: HTTPClientManager
44
+ """
45
+ if cls._instance is None:
46
+ cls._instance = super().__new__(cls)
47
+ return cls._instance
48
+
49
+ def __init__(self):
50
+ """Initialize the HTTP client manager.
51
+
52
+ Sets up default timeout and connection limit configurations,
53
+ initializes internal storage for clients, and sets up
54
+ state tracking for cleanup operations.
55
+ """
56
+ if not hasattr(self, "_initialized"):
57
+ self._clients: Dict[str, httpx.AsyncClient] = {}
58
+ self._default_timeout = httpx.Timeout(
59
+ connect=5.0, read=30.0, write=10.0, pool=5.0
60
+ )
61
+ self._default_limits = httpx.Limits(
62
+ max_keepalive_connections=10,
63
+ max_connections=20,
64
+ keepalive_expiry=30.0,
65
+ )
66
+ self._initialized = True
67
+ self._is_closing = False
68
+
69
+ async def get_client(
70
+ self,
71
+ base_url: Optional[str] = None,
72
+ timeout: Optional[httpx.Timeout] = None,
73
+ limits: Optional[httpx.Limits] = None,
74
+ client_class: Optional[Type[httpx.AsyncClient]] = None,
75
+ **kwargs,
76
+ ) -> httpx.AsyncClient:
77
+ """Get or create an HTTP client for the given configuration.
78
+
79
+ Retrieves a cached client if one exists with matching
80
+ configuration, or creates a new client if needed. The client
81
+ is cached based on a combination of base_url, timeout, limits,
82
+ HTTP version, redirect following settings, and client class.
83
+
84
+ :param base_url: Optional base URL for the client
85
+ :type base_url: Optional[str]
86
+ :param timeout: Optional custom timeout configuration
87
+ :type timeout: Optional[httpx.Timeout]
88
+ :param limits: Optional custom connection limits
89
+ :type limits: Optional[httpx.Limits]
90
+ :param client_class: Optional custom client class (e.g., AuthenticatedClient)
91
+ :type client_class: Optional[Type[httpx.AsyncClient]]
92
+ :param **kwargs: Additional client configuration options
93
+ :return: Configured HTTP client instance
94
+ :rtype: httpx.AsyncClient
95
+ """
96
+ http2_flag = kwargs.get("http2")
97
+ if http2_flag is None:
98
+ http2_flag = os.getenv("HTTP_ENABLE_HTTP2", "false").lower() == "true"
99
+ if http2_flag:
100
+ try:
101
+ import h2 # type: ignore # noqa: F401
102
+ except Exception:
103
+ logger.warning(
104
+ "HTTP/2 requested but 'h2' package not installed; falling back to HTTP/1.1"
105
+ )
106
+ http2_flag = False
107
+ follow = kwargs.get("follow_redirects", True)
108
+
109
+ def timeout_key(t: Optional[httpx.Timeout]):
110
+ if not t:
111
+ return None
112
+ return (t.connect, t.read, t.write, t.pool)
113
+
114
+ def limits_key(limits_obj: Optional[httpx.Limits]):
115
+ if not limits_obj:
116
+ return None
117
+ return (
118
+ limits_obj.max_keepalive_connections,
119
+ limits_obj.max_connections,
120
+ limits_obj.keepalive_expiry,
121
+ )
122
+
123
+ t_key = timeout_key(timeout)
124
+ l_key = limits_key(limits)
125
+ class_name = (client_class or httpx.AsyncClient).__name__
126
+ cache_key = str(
127
+ (
128
+ base_url or "default",
129
+ t_key,
130
+ l_key,
131
+ http2_flag,
132
+ follow,
133
+ class_name,
134
+ )
135
+ )
136
+
137
+ if cache_key not in self._clients:
138
+ async with self._lock:
139
+ if cache_key not in self._clients:
140
+ client_config: Dict[str, Any] = {
141
+ "timeout": timeout or self._default_timeout,
142
+ "limits": limits or self._default_limits,
143
+ "http2": http2_flag,
144
+ "follow_redirects": follow,
145
+ **kwargs,
146
+ }
147
+ if base_url:
148
+ client_config["base_url"] = base_url
149
+
150
+ # Use the specified client class or default to httpx.AsyncClient
151
+ actual_client_class = client_class or httpx.AsyncClient
152
+ self._clients[cache_key] = actual_client_class(**client_config)
153
+ logger.debug(
154
+ "Created new %s client for %s",
155
+ actual_client_class.__name__,
156
+ cache_key,
157
+ )
158
+
159
+ return self._clients[cache_key]
160
+
161
+ def register_external_client(self, client: httpx.AsyncClient) -> None:
162
+ """Register an external HTTP client for cleanup tracking.
163
+
164
+ Adds an externally created HTTP client to the manager's
165
+ tracking system so it can be properly closed during cleanup.
166
+
167
+ :param client: External HTTP client to track
168
+ :type client: httpx.AsyncClient
169
+ """
170
+ self._external_clients.add(client)
171
+ logger.debug("Registered external client for cleanup tracking")
172
+
173
+ async def close_all(self):
174
+ """Close all managed and external HTTP clients.
175
+
176
+ Safely closes all HTTP clients managed by this instance,
177
+ including both internally created clients and externally
178
+ registered ones. Implements duplicate call protection and
179
+ comprehensive error handling during cleanup.
180
+ """
181
+ if self._is_closing:
182
+ logger.debug("Already closing HTTP clients, skipping duplicate call")
183
+ return
184
+
185
+ self._is_closing = True
186
+ try:
187
+ total_clients = len(self._clients) + len(self._external_clients)
188
+ if total_clients == 0:
189
+ logger.debug("No HTTP clients to close")
190
+ return
191
+ logger.info("Closing %d HTTP client(s)...", total_clients)
192
+ for cache_key, client in list(self._clients.items()):
193
+ try:
194
+ await client.aclose()
195
+ logger.debug("Closed managed HTTP client: %s", cache_key)
196
+ except Exception as e:
197
+ logger.warning(
198
+ "Error closing managed HTTP client %s: %s",
199
+ cache_key,
200
+ e,
201
+ )
202
+ for client in list(self._external_clients):
203
+ try:
204
+ await client.aclose()
205
+ logger.debug("Closed external HTTP client")
206
+ except Exception as e:
207
+ logger.warning("Error closing external HTTP client: %s", e)
208
+ self._clients.clear()
209
+ self._external_clients.clear()
210
+ logger.info("All HTTP clients closed successfully")
211
+ finally:
212
+ self._is_closing = False
213
+
214
+
215
+ http_client_manager = HTTPClientManager()
216
+
217
+
218
+ async def get_http_client(
219
+ authenticated: bool = False,
220
+ auth_manager=None,
221
+ media_registry=None,
222
+ header_resolver=None,
223
+ **kwargs,
224
+ ) -> httpx.AsyncClient:
225
+ """Get an HTTP client with the specified configuration.
226
+
227
+ Convenience function that delegates to the global HTTP client
228
+ manager to retrieve or create an HTTP client. Optionally creates
229
+ an AuthenticatedClient for Amazon Ads API calls.
230
+
231
+ :param authenticated: Whether to use AuthenticatedClient
232
+ :type authenticated: bool
233
+ :param auth_manager: Authentication manager (required if authenticated=True)
234
+ :type auth_manager: Optional[AuthManager]
235
+ :param media_registry: Media type registry for content negotiation
236
+ :type media_registry: Optional[MediaTypeRegistry]
237
+ :param header_resolver: Header name resolver
238
+ :type header_resolver: Optional[HeaderNameResolver]
239
+ :param **kwargs: Client configuration parameters
240
+ :return: Configured HTTP client instance
241
+ :rtype: httpx.AsyncClient
242
+ """
243
+ if authenticated:
244
+ # Import here to avoid circular dependency
245
+ from ..http_client import AuthenticatedClient
246
+
247
+ # Extract standard httpx client params
248
+ httpx_kwargs = {
249
+ k: v
250
+ for k, v in kwargs.items()
251
+ if k not in ["auth_manager", "media_registry", "header_resolver"]
252
+ }
253
+
254
+ # Add auth-specific params to kwargs for the AuthenticatedClient constructor
255
+ httpx_kwargs["auth_manager"] = auth_manager
256
+ httpx_kwargs["media_registry"] = media_registry
257
+ httpx_kwargs["header_resolver"] = header_resolver
258
+
259
+ return await http_client_manager.get_client(
260
+ client_class=AuthenticatedClient, **httpx_kwargs
261
+ )
262
+
263
+ return await http_client_manager.get_client(**kwargs)
264
+
265
+
266
+ def create_timeout(
267
+ connect: float = 5.0,
268
+ read: float = 30.0,
269
+ write: float = 10.0,
270
+ pool: float = 5.0,
271
+ ) -> httpx.Timeout:
272
+ """Create a timeout configuration object.
273
+
274
+ :param connect: Connection timeout in seconds
275
+ :type connect: float
276
+ :param read: Read timeout in seconds
277
+ :type read: float
278
+ :param write: Write timeout in seconds
279
+ :type write: float
280
+ :param pool: Pool timeout in seconds
281
+ :type pool: float
282
+ :return: Configured timeout object
283
+ :rtype: httpx.Timeout
284
+ """
285
+ return httpx.Timeout(connect=connect, read=read, write=write, pool=pool)
286
+
287
+
288
+ def create_limits(
289
+ max_keepalive_connections: int = 10,
290
+ max_connections: int = 20,
291
+ keepalive_expiry: float = 30.0,
292
+ ) -> httpx.Limits:
293
+ """Create a connection limits configuration object.
294
+
295
+ :param max_keepalive_connections: Maximum number of keepalive connections
296
+ :type max_keepalive_connections: int
297
+ :param max_connections: Maximum total number of connections
298
+ :type max_connections: int
299
+ :param keepalive_expiry: Keepalive connection expiry time in seconds
300
+ :type keepalive_expiry: float
301
+ :return: Configured limits object
302
+ :rtype: httpx.Limits
303
+ """
304
+ return httpx.Limits(
305
+ max_keepalive_connections=max_keepalive_connections,
306
+ max_connections=max_connections,
307
+ keepalive_expiry=keepalive_expiry,
308
+ )
309
+
310
+
311
+ async def health_check(url: str, timeout: float = 5.0) -> bool:
312
+ """Perform a health check on a URL.
313
+
314
+ Makes a simple GET request to the specified URL to check if
315
+ the service is responding with a successful status code.
316
+
317
+ :param url: URL to perform health check on
318
+ :type url: str
319
+ :param timeout: Timeout for the health check request in seconds
320
+ :type timeout: float
321
+ :return: True if health check passes, False otherwise
322
+ :rtype: bool
323
+ """
324
+ try:
325
+ async with httpx.AsyncClient(timeout=timeout) as client:
326
+ r = await client.get(url)
327
+ return 200 <= r.status_code < 300
328
+ except Exception:
329
+ return False