Skip to content

Commit dd44577

Browse files
jsonbaileyclaude
andcommitted
feat!: Return Result from from_resumption_token instead of raising
from_resumption_token and LDAIClient.create_tracker now return ldclient.Result instead of raising ValueError on invalid tokens, letting callers handle errors without try/except. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 04f14eb commit dd44577

3 files changed

Lines changed: 32 additions & 25 deletions

File tree

packages/sdk/server-ai/src/ldai/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Any, Callable, Dict, List, Optional, Tuple
33

44
import chevron
5-
from ldclient import Context
5+
from ldclient import Context, Result
66
from ldclient.client import LDClient
77

88
from ldai import log
@@ -62,7 +62,7 @@ def __init__(self, client: LDClient):
6262
1,
6363
)
6464

65-
def create_tracker(self, token: str, context: Context) -> LDAIConfigTracker:
65+
def create_tracker(self, token: str, context: Context) -> Result:
6666
"""
6767
Reconstruct a tracker from a resumption token.
6868
@@ -71,9 +71,9 @@ def create_tracker(self, token: str, context: Context) -> LDAIConfigTracker:
7171
:param token: A URL-safe Base64-encoded resumption token obtained from
7272
:attr:`LDAIConfigTracker.resumption_token`.
7373
:param context: The context to use for track events.
74-
:return: A new :class:`LDAIConfigTracker` bound to the original
75-
``runId`` from the token.
76-
:raises ValueError: If the token is invalid or missing required fields.
74+
:return: A :class:`Result` whose ``value`` is a new
75+
:class:`LDAIConfigTracker` on success, or whose ``error`` describes
76+
the problem on failure.
7777
"""
7878
return LDAIConfigTracker.from_resumption_token(token, self._client, context)
7979

packages/sdk/server-ai/src/ldai/tracker.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from enum import Enum
66
from typing import Any, Callable, Dict, Iterable, List, Optional
77

8-
from ldclient import Context, LDClient
8+
from ldclient import Context, LDClient, Result
99

1010
from ldai import log
1111

@@ -132,7 +132,7 @@ def resumption_token(self) -> str:
132132
return base64.urlsafe_b64encode(payload.encode("utf-8")).rstrip(b"=").decode("utf-8")
133133

134134
@classmethod
135-
def from_resumption_token(cls, token: str, ld_client: LDClient, context: Context) -> 'LDAIConfigTracker':
135+
def from_resumption_token(cls, token: str, ld_client: LDClient, context: Context) -> Result:
136136
"""
137137
Reconstruct a tracker from a resumption token.
138138
@@ -144,25 +144,25 @@ def from_resumption_token(cls, token: str, ld_client: LDClient, context: Context
144144
:attr:`resumption_token`.
145145
:param ld_client: LaunchDarkly client instance.
146146
:param context: The context to use for track events.
147-
:return: A new :class:`LDAIConfigTracker` bound to the original
148-
``runId`` from the token.
149-
:raises ValueError: If the token is invalid or missing required fields.
147+
:return: A :class:`Result` whose ``value`` is a new
148+
:class:`LDAIConfigTracker` bound to the original ``runId`` from the
149+
token on success, or whose ``error`` describes the problem on failure.
150150
"""
151151
try:
152152
padded = token + "=" * (-len(token) % 4)
153153
payload = json.loads(
154154
base64.urlsafe_b64decode(padded.encode("utf-8")).decode("utf-8")
155155
)
156156
except (json.JSONDecodeError, Exception) as e:
157-
raise ValueError(f"Invalid resumption token: {e}") from e
157+
return Result.fail(f"Invalid resumption token: {e}", e)
158158

159159
for field in ("runId", "configKey", "version"):
160160
if field not in payload:
161-
raise ValueError(
161+
return Result.fail(
162162
f"Invalid resumption token: missing required field '{field}'"
163163
)
164164

165-
return cls(
165+
return Result.success(cls(
166166
ld_client=ld_client,
167167
run_id=payload["runId"],
168168
config_key=payload["configKey"],
@@ -172,7 +172,7 @@ def from_resumption_token(cls, token: str, ld_client: LDClient, context: Context
172172
model_name="",
173173
provider_name="",
174174
graph_key=payload.get("graphKey"),
175-
)
175+
))
176176

177177
def __get_track_data(self) -> dict:
178178
"""

packages/sdk/server-ai/tests/test_tracker.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,9 @@ def test_resumption_token_round_trip_with_graph_key(client: LDClient):
788788
)
789789

790790
token = tracker.resumption_token
791-
restored = LDAIConfigTracker.from_resumption_token(token, client, context)
791+
result = LDAIConfigTracker.from_resumption_token(token, client, context)
792+
assert result.is_success()
793+
restored = result.value
792794

793795
assert restored._run_id == "test-run-id"
794796
assert restored._config_key == "cfg-key"
@@ -829,7 +831,9 @@ def test_client_create_tracker_from_resumption_token():
829831
token = original.resumption_token
830832

831833
# Reconstruct from token
832-
restored = ai_client.create_tracker(token, context)
834+
result = ai_client.create_tracker(token, context)
835+
assert result.is_success()
836+
restored = result.value
833837

834838
# The restored tracker should use the same runId
835839
restored.track_feedback({"kind": FeedbackKind.Positive})
@@ -851,7 +855,7 @@ def test_client_create_tracker_from_resumption_token():
851855
assert feedback_calls[0].args[1] == context
852856

853857

854-
def test_client_create_tracker_raises_on_invalid_base64():
858+
def test_client_create_tracker_fails_on_invalid_base64():
855859
from unittest.mock import Mock
856860

857861
from ldai.client import LDAIClient
@@ -860,11 +864,12 @@ def test_client_create_tracker_raises_on_invalid_base64():
860864
ai_client = LDAIClient(mock_client)
861865
context = Context.create("user-key")
862866

863-
with pytest.raises(ValueError, match="Invalid resumption token"):
864-
ai_client.create_tracker("not-valid-base64!!!", context)
867+
result = ai_client.create_tracker("not-valid-base64!!!", context)
868+
assert not result.is_success()
869+
assert "Invalid resumption token" in result.error
865870

866871

867-
def test_client_create_tracker_raises_on_missing_fields():
872+
def test_client_create_tracker_fails_on_missing_fields():
868873
import base64
869874
import json
870875

@@ -881,11 +886,12 @@ def test_client_create_tracker_raises_on_missing_fields():
881886
json.dumps({"configKey": "k", "version": 1}).encode()
882887
).rstrip(b"=").decode()
883888

884-
with pytest.raises(ValueError, match="missing required field 'runId'"):
885-
ai_client.create_tracker(incomplete, context)
889+
result = ai_client.create_tracker(incomplete, context)
890+
assert not result.is_success()
891+
assert "missing required field 'runId'" in result.error
886892

887893

888-
def test_client_create_tracker_raises_on_invalid_json():
894+
def test_client_create_tracker_fails_on_invalid_json():
889895
import base64
890896

891897
from unittest.mock import Mock
@@ -898,5 +904,6 @@ def test_client_create_tracker_raises_on_invalid_json():
898904

899905
bad_token = base64.urlsafe_b64encode(b"not json").rstrip(b"=").decode()
900906

901-
with pytest.raises(ValueError, match="Invalid resumption token"):
902-
ai_client.create_tracker(bad_token, context)
907+
result = ai_client.create_tracker(bad_token, context)
908+
assert not result.is_success()
909+
assert "Invalid resumption token" in result.error

0 commit comments

Comments
 (0)