Repository navigation
Add configurable HTTP timeout and nullable mark condition support to the Python client #3301
Description
Background
We are planning to migrate LiveHint AI from direct UpGrade v6 API requests to the Python client introduced in #3146.
LiveHint currently calls the following endpoints directly:
POST /api/v6/initPOST /api/v6/assignPOST /api/v6/markPOST /api/v6/reward
Most of these calls map cleanly to the Python client. However, two client limitations prevent us from preserving the existing application behavior:
- The Python client does not expose an HTTP timeout configuration.
UpgradeClient.mark_decision_point()requirescondition: str, even though a missing assignment should be marked with a null condition.
1. Add configurable HTTP timeout support
Current behavior
ApiService creates new httpx.Client and httpx.AsyncClient instances without specifying a timeout:
async with httpx.AsyncClient() as client:
...
with httpx.Client() as client:
...As a result, the client uses httpx's default timeout.
LiveHint currently uses a 0.5-second timeout for UpGrade requests. Migrating without configurable timeout support would increase the failure-path latency from approximately 0.5 seconds to the httpx default.
Because these calls occur synchronously in user-facing Flask routes, this would be a user-visible behavioral change.
Proposed API
Add an optional timeout to UpgradeClient:
client = UpgradeClient(
user_id="user-123",
host_url="https://upgrade.example.com",
context="livehint-ai",
timeout=0.5,
)Suggested constructor signature:
def __init__(
self,
user_id: str,
host_url: str,
context: str,
token: str = "",
client_session_id: str | None = None,
timeout: float | httpx.Timeout = 5.0,
) -> None:The value should be passed to ApiService and used by both HTTP clients:
async with httpx.AsyncClient(timeout=self._timeout) as client:
...
with httpx.Client(timeout=self._timeout) as client:
...The default should preserve the current effective behavior for existing Python client consumers.
2. Allow a null condition when marking no assignment
Current behavior
The backend and JavaScript client support marking a decision point without a condition when no experiment assignment exists.
Conceptually, the request is:
{
"status": "no condition assigned",
"data": {
"site": "problem-info",
"target": "mathbook_tx",
"assignedCondition": {
"conditionCode": null
}
}
}The Python client currently declares condition and condition_code as str in:
UpgradeClient.mark_decision_pointUpgradeClient.mark_decision_point_syncApiService.mark_decision_pointApiService.mark_decision_point_syncApiService._build_mark_body
This makes the following valid use case incompatible with the public type contract:
client.mark_decision_point_sync(
condition=None,
status=UpgradeClient.MARKED_DECISION_POINT_STATUS.NO_CONDITION_ASSIGNED,
site="problem-info",
target="mathbook_tx",
)Although None may currently pass through at runtime, callers should not need to violate the documented type contract.
Proposed change
Update the relevant public and internal method signatures to accept:
condition: str | Noneand:
condition_code: str | NoneWhen the condition is None, the serialized request should send:
"assignedCondition": {
"conditionCode": null
}This should match the backend and JavaScript client behavior for NO_CONDITION_ASSIGNED.
Tests
Please add coverage for the following cases.
Timeout
- A custom numeric timeout is forwarded to
httpx.AsyncClient. - A custom numeric timeout is forwarded to
httpx.Client. - An
httpx.Timeoutobject is accepted. - Omitting the timeout preserves the current default behavior.
- Timeout exceptions continue to propagate consistently from sync and async APIs.
Nullable condition
- Async
mark_decision_pointacceptscondition=None. - Sync
mark_decision_point_syncacceptscondition=None. - The request contains
"conditionCode": null. - The request uses the
NO_CONDITION_ASSIGNEDstatus. - Normal non-null condition marking remains unchanged.
- Assignment-based marking through
Assignment.mark_decision_pointremains unchanged.
Documentation
Update the Python client README to document:
- The new optional
timeoutconstructor parameter. - A no-assignment marking example using
condition=None. - The default timeout behavior.
Example:
client = UpgradeClient(
user_id="user-123",
host_url="https://upgrade.example.com",
context="my-app",
timeout=0.5,
)
client.init_sync()
assignment = client.get_decision_point_assignment_sync(
site="problem-info",
target="mathbook_tx",
)
if assignment is None:
client.mark_decision_point_sync(
condition=None,
status=UpgradeClient.MARKED_DECISION_POINT_STATUS.NO_CONDITION_ASSIGNED,
site="problem-info",
target="mathbook_tx",
)Acceptance criteria
UpgradeClientexposes a backward-compatible timeout option.- The configured timeout is used by both synchronous and asynchronous HTTP requests.
- Public mark methods accept
Noneas the condition. - A null condition is serialized correctly for
NO_CONDITION_ASSIGNED. - Existing client behavior remains backward compatible.
- Python client unit tests, type checking, and linting pass.
- The changes are included in a published PyPI patch release so downstream applications can pin the fixed version.
Downstream use case
Once released, LiveHint AI will use the client for the complete flow:
init → get decision-point assignment → validate payload → mark
It will also replace its direct reward request with send_reward_sync().
Configurable timeout and nullable-condition support will allow that migration without changing LiveHint's existing failure latency or no-assignment telemetry.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsCode Review
Activity