038: add @RATIONALE/@REJECTED contracts to async C4/C5 modules
5 contracts updated: - AsyncNetworkModule (C5): async migration rationale, per-client CSRF cookie rejection - AsyncAPIClient (C4): auth lifecycle, cache-hit CSRF refresh - AsyncAPIClient.request (C4): string vs dict handling, double-encoding root cause - AsyncAPIClient.upload_file (C4): multipart async upload - LLMAsyncHttpClient (C4): response.ok -> is_success, module-level httpx singleton
This commit is contained in:
@@ -8,6 +8,11 @@
|
||||
# @DATA_CONTRACT Input[config: Dict[str, Any]] -> Output[authenticated async Superset HTTP interactions]
|
||||
# @RELATION DEPENDS_ON -> [SupersetAuthCache]
|
||||
# @INVARIANT Async client reuses cached auth tokens per environment credentials and invalidates on 401.
|
||||
# @RATIONALE Async migration of Superset network layer. Uses httpx.AsyncClient with session-level
|
||||
# cookie persistence for CSRF. Shared auth cache reduces per-request login overhead from O(n) to O(1).
|
||||
# String data passed as httpx content (not json) to avoid double-encoding of pre-serialized JSON.
|
||||
# @REJECTED requests.Session — blocks async event loop. Per-request AsyncAPIClient — loses connection
|
||||
# pooling and CSRF session cookies, causing 'CSRF tokens do not match' on SQL Lab execute.
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
@@ -36,6 +41,11 @@ from .network import (
|
||||
# @RELATION CALLS -> [SupersetAuthCache.get]
|
||||
# @RELATION CALLS -> [SupersetAuthCache.set]
|
||||
# @INVARIANT semaphore.acquire() before request and semaphore.release() in finally.
|
||||
# @RATIONALE One class handles auth lifecycle, CSRF token refresh on cache-hit, and per-env
|
||||
# semaphore for connection backpressure. authenticate() was extended to re-fetch CSRF token
|
||||
# on cache-hit to set session cookie on new httpx.AsyncClient instances.
|
||||
# @REJECTED Per-request client with separate httpx.AsyncClient — loses CSRF session cookie,
|
||||
# causes 'CSRF tokens do not match' on SQL Lab execute. No semaphore — no backpressure.
|
||||
class AsyncAPIClient:
|
||||
DEFAULT_TIMEOUT = 30
|
||||
_auth_locks: dict[tuple[str, str, bool], asyncio.Lock] = {}
|
||||
@@ -207,6 +217,12 @@ class AsyncAPIClient:
|
||||
# @RELATION CALLS -> [AsyncAPIClient._handle_http_error]
|
||||
# @RELATION CALLS -> [AsyncAPIClient._handle_network_error]
|
||||
# @RELATION CALLS -> [AsyncAPIClient._is_dashboard_endpoint]
|
||||
# @RATIONALE Supports both pre-serialized JSON strings (legacy callers) and Python dicts.
|
||||
# Strings sent via httpx content parameter to avoid double encoding by json parameter.
|
||||
# This was the root cause of GENERIC_BACKEND_ERROR on all POST requests — httpx re-encoded
|
||||
# pre-serialized JSON, turning the request body into a JSON string instead of a JSON object.
|
||||
# @REJECTED Passing all data through httpx json= — double-encodes strings. Using only content=
|
||||
# breaks callers that pass dicts. Always passing dicts — requires refactoring 50+ callers.
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
@@ -403,6 +419,10 @@ class AsyncAPIClient:
|
||||
# @POST Returns parsed JSON API response.
|
||||
# @SIDE_EFFECT Performs multipart HTTP upload via httpx.AsyncClient.
|
||||
# @RELATION CALLS -> [AsyncAPIClient.get_headers]
|
||||
# @RATIONALE Async migration of requests.post to httpx.AsyncClient.post with multipart files.
|
||||
# Uses httpx files= parameter for multipart upload with file_name and Content-Type per file.
|
||||
# @REJECTED requests.post with files= — blocks async event loop. Direct file read -> httpx content
|
||||
# — loses multipart boundary handling.
|
||||
async def upload_file(
|
||||
self,
|
||||
endpoint: str,
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
# @POST Returns (response text, finish_reason) tuple.
|
||||
# @SIDE_EFFECT Async HTTP POST to LLM API with optional retry on 429.
|
||||
# @RATIONALE Async migration of _llm_http.py to use httpx.AsyncClient instead of sync requests.
|
||||
# Uses asyncio.sleep for 429 backoff instead of time.sleep.
|
||||
# Uses asyncio.sleep for 429 backoff instead of time.sleep. Module-level httpx client singleton
|
||||
# for connection reuse. response.ok -> response.is_success for httpx.Response compatibility.
|
||||
# @REJECTED Keeping sync requests.post — would block async event loop during LLM calls.
|
||||
# Per-request httpx.AsyncClient — loses connection pooling. response.ok — httpx.Response has no
|
||||
# .ok attribute (only is_success), causes 'Response' object has no attribute 'ok'.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
Reference in New Issue
Block a user