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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions