asap-protocol 0.5.0__py3-none-any.whl → 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 (59) hide show
  1. asap/__init__.py +1 -1
  2. asap/cli.py +137 -2
  3. asap/examples/README.md +81 -13
  4. asap/examples/auth_patterns.py +212 -0
  5. asap/examples/error_recovery.py +248 -0
  6. asap/examples/long_running.py +287 -0
  7. asap/examples/mcp_integration.py +240 -0
  8. asap/examples/multi_step_workflow.py +134 -0
  9. asap/examples/orchestration.py +293 -0
  10. asap/examples/rate_limiting.py +137 -0
  11. asap/examples/run_demo.py +0 -2
  12. asap/examples/secure_handler.py +84 -0
  13. asap/examples/state_migration.py +240 -0
  14. asap/examples/streaming_response.py +108 -0
  15. asap/examples/websocket_concept.py +129 -0
  16. asap/mcp/__init__.py +43 -0
  17. asap/mcp/client.py +224 -0
  18. asap/mcp/protocol.py +179 -0
  19. asap/mcp/server.py +333 -0
  20. asap/mcp/server_runner.py +40 -0
  21. asap/models/base.py +0 -3
  22. asap/models/constants.py +3 -1
  23. asap/models/entities.py +21 -6
  24. asap/models/envelope.py +7 -0
  25. asap/models/ids.py +8 -4
  26. asap/models/parts.py +33 -3
  27. asap/models/validators.py +16 -0
  28. asap/observability/__init__.py +6 -0
  29. asap/observability/dashboards/README.md +24 -0
  30. asap/observability/dashboards/asap-detailed.json +131 -0
  31. asap/observability/dashboards/asap-red.json +129 -0
  32. asap/observability/logging.py +81 -1
  33. asap/observability/metrics.py +15 -1
  34. asap/observability/trace_parser.py +238 -0
  35. asap/observability/trace_ui.py +218 -0
  36. asap/observability/tracing.py +293 -0
  37. asap/state/machine.py +15 -2
  38. asap/state/snapshot.py +0 -9
  39. asap/testing/__init__.py +31 -0
  40. asap/testing/assertions.py +108 -0
  41. asap/testing/fixtures.py +113 -0
  42. asap/testing/mocks.py +152 -0
  43. asap/transport/__init__.py +28 -0
  44. asap/transport/cache.py +180 -0
  45. asap/transport/circuit_breaker.py +9 -8
  46. asap/transport/client.py +418 -36
  47. asap/transport/compression.py +389 -0
  48. asap/transport/handlers.py +106 -53
  49. asap/transport/middleware.py +58 -34
  50. asap/transport/server.py +429 -139
  51. asap/transport/validators.py +0 -4
  52. asap/utils/sanitization.py +0 -5
  53. asap_protocol-1.0.0.dist-info/METADATA +264 -0
  54. asap_protocol-1.0.0.dist-info/RECORD +70 -0
  55. asap_protocol-0.5.0.dist-info/METADATA +0 -244
  56. asap_protocol-0.5.0.dist-info/RECORD +0 -41
  57. {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
  58. {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
  59. {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -32,8 +32,10 @@ Example:
32
32
  >>> middleware = AuthenticationMiddleware(manifest, validator)
33
33
  """
34
34
 
35
+ import asyncio
36
+ import inspect
35
37
  import uuid
36
- from typing import Any, Awaitable, Callable, Protocol
38
+ from typing import Any, Awaitable, Callable, Protocol, cast
37
39
  from collections.abc import Sequence
38
40
 
39
41
  from fastapi import HTTPException, Request
@@ -54,7 +56,9 @@ logger = get_logger(__name__)
54
56
  AUTH_SCHEME_BEARER = "bearer"
55
57
 
56
58
  # Rate limiting default configuration
57
- DEFAULT_RATE_LIMIT = "100/minute"
59
+ # Uses token bucket pattern: burst limit (per second) + sustained limit (per minute)
60
+ # This allows short bursts while preventing sustained abuse
61
+ DEFAULT_RATE_LIMIT = "10/second;100/minute"
58
62
 
59
63
 
60
64
  def _get_sender_from_envelope(request: Request) -> str:
@@ -80,9 +84,7 @@ def _get_sender_from_envelope(request: Request) -> str:
80
84
  >>> sender = _get_sender_from_envelope(request)
81
85
  >>> # Returns "192.168.1.1" (IP address, not sender URN)
82
86
  """
83
- # Try to extract sender from envelope if already parsed (early returns reduce complexity)
84
87
  try:
85
- # Check if envelope is stored in request state (after parsing)
86
88
  if hasattr(request.state, "envelope") and request.state.envelope:
87
89
  envelope = request.state.envelope
88
90
  if hasattr(envelope, "sender") and isinstance(envelope.sender, str):
@@ -105,17 +107,12 @@ def _get_sender_from_envelope(request: Request) -> str:
105
107
  # Envelope not available, fall back to IP
106
108
  pass
107
109
 
108
- # Fallback to client IP address
109
110
  remote_addr = get_remote_address(request)
110
- # Type narrowing: get_remote_address returns str, but mypy may see it as Any
111
111
  if isinstance(remote_addr, str):
112
112
  return remote_addr
113
113
  return str(remote_addr)
114
114
 
115
115
 
116
- # Create rate limiter instance with IP-based key function
117
- # Note: The key function attempts to extract sender but always falls back to IP
118
- # because rate limiting executes before request body parsing
119
116
  limiter = Limiter(
120
117
  key_func=_get_sender_from_envelope,
121
118
  default_limits=[DEFAULT_RATE_LIMIT],
@@ -154,8 +151,15 @@ def create_test_limiter(limits: Sequence[str] | None = None) -> Limiter:
154
151
  def create_limiter(limits: Sequence[str] | None = None) -> Limiter:
155
152
  """Create a new limiter instance for production use.
156
153
 
157
- Creates an isolated limiter instance with its own storage, allowing
158
- multiple FastAPI app instances to have independent rate limiters.
154
+ Creates an isolated limiter instance with its own in-memory storage
155
+ (``memory://``), allowing multiple FastAPI app instances to have
156
+ independent rate limiters.
157
+
158
+ **Multi-worker warning:** ``memory://`` storage is per-process. In
159
+ multi-worker deployments (e.g., Gunicorn with 4 workers), each worker
160
+ has isolated limits—effective rate = configured limit × number of workers
161
+ (e.g., 10/s → 40/s across workers). For shared limits in production,
162
+ use Redis-backed storage via slowapi's ``storage_uri``.
159
163
 
160
164
  Args:
161
165
  limits: Optional list of rate limit strings (e.g., ["100/minute"]).
@@ -173,10 +177,16 @@ def create_limiter(limits: Sequence[str] | None = None) -> Limiter:
173
177
 
174
178
  # Use unique storage URI to ensure isolation between app instances
175
179
  unique_storage_id = str(uuid.uuid4())
180
+ storage_uri = f"memory://{unique_storage_id}"
181
+ logger.warning(
182
+ "asap.rate_limit.memory_storage",
183
+ message="memory:// storage is per-process; in multi-worker deployments "
184
+ "(e.g., Gunicorn), effective rate = limit × workers. Use Redis for shared limits.",
185
+ )
176
186
  return Limiter(
177
187
  key_func=_get_sender_from_envelope,
178
188
  default_limits=list(limits),
179
- storage_uri=f"memory://{unique_storage_id}",
189
+ storage_uri=storage_uri,
180
190
  )
181
191
 
182
192
 
@@ -197,7 +207,6 @@ def rate_limit_handler(request: Request, exc: Exception) -> JSONResponse:
197
207
  >>> response = rate_limit_handler(request, exc)
198
208
  >>> # Returns JSONResponse with status_code=429 and JSON-RPC error
199
209
  """
200
- # Type narrowing: FastAPI passes RateLimitExceeded but handler signature uses Exception
201
210
  if not isinstance(exc, RateLimitExceeded):
202
211
  # Fallback for unexpected exception types
203
212
  logger.warning("asap.rate_limit.unexpected_exception", exc_type=type(exc).__name__)
@@ -269,25 +278,32 @@ class TokenValidator(Protocol):
269
278
  """Protocol for token validation implementations.
270
279
 
271
280
  Custom validators must implement this interface to integrate
272
- with the authentication middleware.
281
+ with the authentication middleware. Validators may be sync or async.
282
+ Sync validators that perform I/O (e.g., DB/Redis lookups) are run
283
+ in a thread pool to avoid blocking the event loop.
273
284
 
274
- Example:
275
- >>> class MyTokenValidator:
285
+ Example (sync):
286
+ >>> class MySyncValidator:
276
287
  ... def __call__(self, token: str) -> str | None:
277
- ... # Validate token against database, JWT, etc.
278
288
  ... if is_valid(token):
279
289
  ... return extract_agent_id(token)
280
290
  ... return None
291
+
292
+ Example (async):
293
+ >>> class MyAsyncValidator:
294
+ ... async def __call__(self, token: str) -> str | None:
295
+ ... return await db.lookup_agent(token)
281
296
  """
282
297
 
283
- def __call__(self, token: str) -> str | None:
298
+ def __call__(self, token: str) -> str | None | Awaitable[str | None]:
284
299
  """Validate a token and return the authenticated agent ID.
285
300
 
286
301
  Args:
287
302
  token: The authentication token to validate
288
303
 
289
304
  Returns:
290
- The agent ID (URN) if token is valid, None otherwise
305
+ The agent ID (URN) if token is valid, None otherwise.
306
+ May return a coroutine for async validators.
291
307
 
292
308
  Example:
293
309
  >>> validator = BearerTokenValidator(my_validate_func)
@@ -301,15 +317,14 @@ class BearerTokenValidator:
301
317
  """Default Bearer token validator implementation.
302
318
 
303
319
  Wraps a validation function to conform to the TokenValidator protocol.
304
- The validation function should take a token string and return an agent ID
305
- if valid, or None if invalid.
320
+ The validation function may be sync or async. Sync validators that perform
321
+ I/O are run in a thread pool by the middleware to avoid blocking the loop.
306
322
 
307
323
  Attributes:
308
324
  validate_func: Function that validates tokens and returns agent IDs
309
325
 
310
326
  Example:
311
327
  >>> def my_validator(token: str) -> str | None:
312
- ... # Check token in database
313
328
  ... if token in valid_tokens:
314
329
  ... return valid_tokens[token]["agent_id"]
315
330
  ... return None
@@ -318,22 +333,28 @@ class BearerTokenValidator:
318
333
  >>> agent_id = validator("abc123")
319
334
  """
320
335
 
321
- def __init__(self, validate_func: Callable[[str], str | None]) -> None:
336
+ def __init__(
337
+ self,
338
+ validate_func: Callable[[str], str | None | Awaitable[str | None]],
339
+ ) -> None:
322
340
  """Initialize the Bearer token validator.
323
341
 
324
342
  Args:
325
- validate_func: Function that validates tokens and returns agent IDs
343
+ validate_func: Sync or async function that validates tokens
344
+ and returns agent IDs. Sync functions with I/O are run
345
+ in a thread pool to avoid blocking the event loop.
326
346
  """
327
347
  self.validate_func = validate_func
328
348
 
329
- def __call__(self, token: str) -> str | None:
349
+ def __call__(self, token: str) -> str | None | Awaitable[str | None]:
330
350
  """Validate a token and return the authenticated agent ID.
331
351
 
332
352
  Args:
333
353
  token: The authentication token to validate
334
354
 
335
355
  Returns:
336
- The agent ID (URN) if token is valid, None otherwise
356
+ The agent ID (URN) if token is valid, None otherwise.
357
+ May return a coroutine for async validate_func.
337
358
  """
338
359
  return self.validate_func(token)
339
360
 
@@ -388,7 +409,6 @@ class AuthenticationMiddleware:
388
409
  self.validator = validator
389
410
  self.security = HTTPBearer(auto_error=False)
390
411
 
391
- # Validate configuration
392
412
  if self._is_auth_required() and validator is None:
393
413
  raise ValueError(
394
414
  "Token validator required when authentication is configured in manifest"
@@ -461,7 +481,6 @@ class AuthenticationMiddleware:
461
481
  headers={"WWW-Authenticate": "Bearer"},
462
482
  )
463
483
 
464
- # Validate Authorization scheme (case-insensitive)
465
484
  if credentials.scheme.lower() != AUTH_SCHEME_BEARER:
466
485
  logger.warning(
467
486
  "asap.auth.invalid_scheme",
@@ -475,7 +494,6 @@ class AuthenticationMiddleware:
475
494
  headers={"WWW-Authenticate": "Bearer"},
476
495
  )
477
496
 
478
- # Validate Bearer token support in manifest
479
497
  if not self._supports_bearer_auth():
480
498
  logger.warning(
481
499
  "asap.auth.scheme_not_supported",
@@ -488,15 +506,23 @@ class AuthenticationMiddleware:
488
506
  headers={"WWW-Authenticate": "Bearer"},
489
507
  )
490
508
 
491
- # Validate token and get agent ID
492
509
  token = credentials.credentials
493
- # Type narrowing: validator is not None when auth is required (validated in __init__)
494
510
  if self.validator is None:
495
511
  raise RuntimeError(
496
512
  "Token validator is None but authentication is required. "
497
513
  "This should not happen if middleware was initialized correctly."
498
514
  )
499
- agent_id = self.validator(token)
515
+ if inspect.iscoroutinefunction(self.validator.__call__):
516
+ result = self.validator(token)
517
+ agent_id = await cast(Awaitable[str | None], result)
518
+ else:
519
+ # Cast: to_thread expects Callable[..., T]; TokenValidator may return Awaitable
520
+ # but we handle that below (BearerTokenValidator wrapping async func)
521
+ agent_id = await asyncio.to_thread(
522
+ cast("Callable[[str], str | None]", self.validator), token
523
+ )
524
+ if inspect.isawaitable(agent_id):
525
+ agent_id = await cast(Awaitable[str | None], agent_id)
500
526
 
501
527
  if agent_id is None:
502
528
  # Log sanitized token to avoid exposing full token data
@@ -615,7 +641,6 @@ class SizeLimitMiddleware(BaseHTTPMiddleware):
615
641
  Returns:
616
642
  Response from next handler or error response if size exceeded
617
643
  """
618
- # Check Content-Length header if present
619
644
  content_length = request.headers.get("content-length")
620
645
  if content_length:
621
646
  try:
@@ -634,7 +659,6 @@ class SizeLimitMiddleware(BaseHTTPMiddleware):
634
659
  },
635
660
  )
636
661
  except ValueError:
637
- # Invalid Content-Length header, let route handler validate actual body size
638
662
  pass
639
663
 
640
664
  # Continue to next middleware or route handler