Skip to content

Implement AsyncMlbDataAdapter #301

Description

@Mattsface

Parent: #297
Depends on: #298, #300

Goal

Implement the asynchronous transport layer for MLB Stats API requests while preserving the behavioral contract established for the synchronous adapter.

Scope

Implement AsyncMlbDataAdapter with:

  • Async GET requests
  • Library-owned async client creation
  • Caller-injected async client support
  • aclose() cleanup
  • Ownership tracking
  • Timeout handling
  • Transport error mapping
  • HTTP error mapping
  • JSON decode errors
  • Strict HTTP default behavior
  • Explicit strict_http=False compatibility behavior
  • Endpoint-specific 404 behavior
  • Retry behavior defined by Define the v1.1 async API and transport contract #298
  • User-Agent behavior consistent with the installed package version
  • Cancellation behavior defined by Define the v1.1 async API and transport contract #298
  • Same-event-loop concurrent request support

Out of scope

Async context-manager support is not part of AsyncMlbDataAdapter in #301. Public async with AsyncMlb() lifecycle support belongs to the AsyncMlb vertical slice in #303. The adapter only needs correct explicit aclose() ownership and cleanup semantics here.

Contract requirements from #298

Lifecycle and ownership

  • Library-created async clients are library-owned, configured by the library, and closed by aclose()
  • Caller-injected clients are caller-owned and must not be closed, silently reconfigured, or mutated by the library
  • aclose() is idempotent
  • Cleanup must not suppress or replace the original exception or caller cancellation

Timeout and transport failures

  • Preserve the sync timeout public shape, including scalar values and (connect, read) tuples
  • Preserve DEFAULT_TIMEOUT = (3.05, 30.0)
  • Translate timeout configuration internally without mutating injected clients
  • Async timeout failures raise MlbTimeoutError
  • Other async request/transport failures raise MlbTransportError
  • Underlying HTTP-library exceptions must remain private implementation details

HTTP behavior

After retries are exhausted:

2xx                         -> normal decode / MlbResult
404                         -> empty MlbResult
non-404 4xx + strict=True   -> MlbHttpError
non-404 4xx + strict=False  -> MlbHttpCompatibilityWarning + empty MlbResult
5xx                         -> MlbHttpError
other non-2xx               -> MlbHttpError
timeout                     -> MlbTimeoutError
transport failure           -> MlbTransportError
caller cancellation         -> asyncio.CancelledError
  • Preserve structured MlbHttpError context where available: status, reason, URL, method, and bounded best-effort response context
  • Error-context extraction failures must never replace the original MlbHttpError
  • Compatibility warnings must not leak bodies, headers, credentials, or other sensitive context

JSON behavior

  • Empty successful response bodies produce {} through MlbResult
  • Valid JSON enters the shared parsing/model path
  • Invalid JSON on a successful response raises MlbDecodeError
  • Decode failures are not retried

Retry behavior

For library-owned clients:

  • Retry only GET requests
  • Preserve retryable statuses 429, 500, 502, 503, 504
  • Preserve equivalent total/connect/read/status limits and effective 0.5 backoff policy
  • Respect Retry-After
  • Never retry caller-requested cancellation
  • Do not retry JSON decode or model/parsing failures
  • Backoff must not block the event loop

Caller-injected clients retain caller-controlled retry configuration.

User-Agent

  • Library-owned clients use python-mlb-statsapi/<installed-version> with the existing unknown fallback
  • Caller-injected client headers remain caller-controlled

Concurrency

  • A single adapter/client instance must support multiple concurrent in-flight requests on the same event loop
  • Per-request state must remain independent
  • Cancelling one request must not implicitly cancel unrelated concurrent requests
  • Do not create hidden worker pools, unrelated background work, or automatic fan-out
  • Cross-thread and cross-event-loop use are not required in v1.1

Focused #301 test coverage

Add a focused implementation test suite in tests/test_async_mlb_dataadapter.py that proves the adapter behavior implemented in this issue. The exhaustive async transport contract matrix remains the responsibility of #302.

Basic success

  • 200 JSON response returns an MlbResult
  • Successful empty response body becomes {}

HTTP contract

  • 404 returns an empty MlbResult
  • Strict non-404 4xx raises MlbHttpError
  • Compatibility-mode non-404 4xx emits MlbHttpCompatibilityWarning and returns an empty result
  • Final 5xx raises MlbHttpError
  • Structured HTTP error context is preserved where available

Exceptions

  • HTTPX timeout maps to MlbTimeoutError
  • Other HTTPX request/transport failures map to MlbTransportError
  • Invalid JSON on a successful response raises MlbDecodeError

Lifecycle and ownership

  • Library-owned client is closed by aclose()
  • aclose() is idempotent
  • Caller-injected client is not closed
  • Caller-injected client configuration is not mutated

Configuration

  • Scalar timeout values are translated correctly
  • (connect, read) timeout tuples are translated correctly
  • Library-owned client receives the package User-Agent
  • Caller-injected client headers remain unchanged

Async behavior

  • Caller cancellation propagates as asyncio.CancelledError
  • Multiple basic concurrent requests can safely share the same adapter/client
  • Transient retryable failure can succeed on a later attempt
  • Retry exhaustion raises the correct public error
  • Retry backoff is awaitable and does not block the event loop

The purpose of these tests is to prove the implementation built in #301. Issue #302 should then expand this into the deterministic, exhaustive transport-contract suite used to prevent long-term sync/async drift.

Compatibility

Equivalent responses should map to the same public MlbResult and exception hierarchy used by the synchronous transport wherever practical.

Caller-owned clients must not be closed or unexpectedly reconfigured by the library.

Acceptance criteria

  • Async adapter can perform successful GET requests
  • Focused AsyncMlbDataAdapter implementation tests cover the behavior delivered in Implement AsyncMlbDataAdapter #301
  • Library-owned async client lifecycle is correct and aclose() is idempotent
  • Injected client ownership and configuration are preserved
  • 404 behavior matches the 1.x contract
  • Strict and compatibility non-404 4xx behavior matches Define the v1.1 async API and transport contract #298
  • Final 5xx and other non-2xx behavior matches Define the v1.1 async API and transport contract #298
  • Timeout/transport/decode failures use the public exception hierarchy
  • Structured HTTP error context matches the public sync contract where available
  • Retry behavior matches Define the v1.1 async API and transport contract #298 and does not block the event loop
  • User-Agent behavior is correct
  • Cancellation is not swallowed, remapped, or retried
  • Multiple concurrent requests can safely share one adapter/client on the same event loop
  • Cancelling one concurrent request does not implicitly cancel an unrelated request

Refs #297
Contract: #298

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions