oneshot-python 0.9.0__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,3 +78,6 @@ config/secrets/
78
78
 
79
79
  # Build outputs
80
80
  apps/video-service/out/
81
+
82
+ # Simulate-players pool (auto-generated, contains player IDs)
83
+ scripts/.simulate-pool.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oneshot-python
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -654,7 +654,10 @@ class OneShotClient:
654
654
 
655
655
  if job.get("status") == "completed":
656
656
  self._log("Job completed")
657
- result = job.get("result", job)
657
+ result = job.get("result") or job
658
+ # Propagate request_id into the result so callers always have it
659
+ if isinstance(result, dict) and "request_id" not in result and job.get("request_id"):
660
+ result["request_id"] = job["request_id"]
658
661
  # Optional second phase: keep polling for Apollo phone-reveal
659
662
  # callbacks. Only fires when the caller explicitly opts in
660
663
  # AND the result still has phones_pending=true (set by the
@@ -747,6 +750,8 @@ class OneShotClient:
747
750
  )
748
751
  job = resp.json()
749
752
  last_result = job.get("result", job)
753
+ if isinstance(last_result, dict) and "request_id" not in last_result and job.get("request_id"):
754
+ last_result["request_id"] = job["request_id"]
750
755
  if not self._is_phones_pending(last_result):
751
756
  return last_result
752
757
  except (OneShotError, JobError):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oneshot-python"
3
- version = "0.9.0"
3
+ version = "0.9.2"
4
4
  description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling"
5
5
  readme = {text = "Core Python SDK for the OneShot API", content-type = "text/plain"}
6
6
  license = "MIT"
@@ -0,0 +1,181 @@
1
+ """Tests for request_id propagation in OneShotClient._poll_job.
2
+
3
+ Verifies that `request_id` from the job envelope is injected into the
4
+ final result dict when the result itself doesn't already contain it.
5
+ Mirrors the TypeScript SDK's tests in tests/unit/sdk-request-id.test.ts.
6
+
7
+ Run with:
8
+ cd packages/oneshot-python && uv run pytest tests/test_request_id.py -v
9
+ # or
10
+ cd packages/oneshot-python && python -m pytest tests/test_request_id.py -v
11
+ """
12
+
13
+ import asyncio
14
+ from typing import Any
15
+ from unittest.mock import MagicMock
16
+
17
+ import pytest
18
+
19
+ from oneshot.client import OneShotClient
20
+
21
+ TEST_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
22
+ TEST_BASE_URL = "http://test.local"
23
+ TEST_REQUEST_ID = "req_test_abc123"
24
+
25
+
26
+ def make_client() -> OneShotClient:
27
+ return OneShotClient(
28
+ private_key=TEST_PRIVATE_KEY,
29
+ base_url=TEST_BASE_URL,
30
+ )
31
+
32
+
33
+ # ── Mock httpx.AsyncClient ──────────────────────────────────────────
34
+
35
+
36
+ def _make_mock_response(status_code: int = 200, json_data: Any = None) -> MagicMock:
37
+ resp = MagicMock()
38
+ resp.is_success = 200 <= status_code < 300
39
+ resp.status_code = status_code
40
+ resp.json = MagicMock(return_value=json_data or {})
41
+ resp.text = ""
42
+ return resp
43
+
44
+
45
+ class _MockHttpxClient:
46
+ def __init__(self, responses):
47
+ self.responses = list(responses)
48
+ self.calls = []
49
+
50
+ async def get(self, url, headers=None):
51
+ self.calls.append({"url": url, "headers": headers})
52
+ if not self.responses:
53
+ raise RuntimeError(f"Unexpected get to {url}")
54
+ next_resp = self.responses.pop(0)
55
+ if isinstance(next_resp, Exception):
56
+ raise next_resp
57
+ return next_resp
58
+
59
+
60
+ # ═══════════════════════════════════════════════════════════════════
61
+ # _poll_job request_id injection
62
+ # ═══════════════════════════════════════════════════════════════════
63
+
64
+
65
+ class TestRequestIdPropagation:
66
+ """Verifies that _poll_job injects request_id from the job envelope
67
+ into the result dict."""
68
+
69
+ def test_injects_request_id_when_missing(self):
70
+ """Result without request_id gets it from the job envelope."""
71
+ client = make_client()
72
+ mock_http = _MockHttpxClient([
73
+ _make_mock_response(200, {
74
+ "request_id": TEST_REQUEST_ID,
75
+ "status": "completed",
76
+ "result": {"url": "https://example.com", "markdown": "# Hello"},
77
+ }),
78
+ ])
79
+
80
+ result = asyncio.get_event_loop().run_until_complete(
81
+ client._poll_job(mock_http, TEST_REQUEST_ID, timeout_sec=5)
82
+ )
83
+
84
+ assert result["request_id"] == TEST_REQUEST_ID
85
+ assert result["url"] == "https://example.com"
86
+ assert result["markdown"] == "# Hello"
87
+
88
+ def test_does_not_overwrite_existing_request_id(self):
89
+ """Result that already has request_id keeps its own value."""
90
+ client = make_client()
91
+ mock_http = _MockHttpxClient([
92
+ _make_mock_response(200, {
93
+ "request_id": TEST_REQUEST_ID,
94
+ "status": "completed",
95
+ "result": {
96
+ "request_id": "req_original_keep",
97
+ "url": "https://example.com",
98
+ },
99
+ }),
100
+ ])
101
+
102
+ result = asyncio.get_event_loop().run_until_complete(
103
+ client._poll_job(mock_http, TEST_REQUEST_ID, timeout_sec=5)
104
+ )
105
+
106
+ assert result["request_id"] == "req_original_keep"
107
+
108
+ def test_handles_null_result(self):
109
+ """When result is null, falls back to job envelope (which has request_id)."""
110
+ client = make_client()
111
+ mock_http = _MockHttpxClient([
112
+ _make_mock_response(200, {
113
+ "request_id": TEST_REQUEST_ID,
114
+ "status": "completed",
115
+ "result": None,
116
+ }),
117
+ ])
118
+
119
+ result = asyncio.get_event_loop().run_until_complete(
120
+ client._poll_job(mock_http, TEST_REQUEST_ID, timeout_sec=5)
121
+ )
122
+
123
+ # Falls back to the job dict which has request_id
124
+ assert result["request_id"] == TEST_REQUEST_ID
125
+ assert result["status"] == "completed"
126
+
127
+ def test_handles_non_dict_result(self):
128
+ """Non-dict result (e.g. string) should not crash."""
129
+ client = make_client()
130
+ mock_http = _MockHttpxClient([
131
+ _make_mock_response(200, {
132
+ "request_id": TEST_REQUEST_ID,
133
+ "status": "completed",
134
+ "result": "plain string result",
135
+ }),
136
+ ])
137
+
138
+ result = asyncio.get_event_loop().run_until_complete(
139
+ client._poll_job(mock_http, TEST_REQUEST_ID, timeout_sec=5)
140
+ )
141
+
142
+ # String result — can't inject request_id, returned as-is
143
+ assert result == "plain string result"
144
+
145
+ def test_polls_until_completed(self):
146
+ """Polls through processing states until completed, then injects request_id."""
147
+ client = make_client()
148
+ mock_http = _MockHttpxClient([
149
+ _make_mock_response(200, {"request_id": TEST_REQUEST_ID, "status": "processing"}),
150
+ _make_mock_response(200, {"request_id": TEST_REQUEST_ID, "status": "processing"}),
151
+ _make_mock_response(200, {
152
+ "request_id": TEST_REQUEST_ID,
153
+ "status": "completed",
154
+ "result": {"data": "final"},
155
+ }),
156
+ ])
157
+
158
+ result = asyncio.get_event_loop().run_until_complete(
159
+ client._poll_job(mock_http, TEST_REQUEST_ID, timeout_sec=30)
160
+ )
161
+
162
+ assert len(mock_http.calls) == 3
163
+ assert result["request_id"] == TEST_REQUEST_ID
164
+ assert result["data"] == "final"
165
+
166
+ def test_no_request_id_in_job_envelope(self):
167
+ """If the job envelope has no request_id, result is returned as-is."""
168
+ client = make_client()
169
+ mock_http = _MockHttpxClient([
170
+ _make_mock_response(200, {
171
+ "status": "completed",
172
+ "result": {"data": "no_rid"},
173
+ }),
174
+ ])
175
+
176
+ result = asyncio.get_event_loop().run_until_complete(
177
+ client._poll_job(mock_http, TEST_REQUEST_ID, timeout_sec=5)
178
+ )
179
+
180
+ assert "request_id" not in result
181
+ assert result["data"] == "no_rid"
@@ -549,7 +549,7 @@ wheels = [
549
549
 
550
550
  [[package]]
551
551
  name = "oneshot-python"
552
- version = "0.8.3"
552
+ version = "0.9.0"
553
553
  source = { editable = "." }
554
554
  dependencies = [
555
555
  { name = "eth-account" },
File without changes