xrtm-data 0.7.2__tar.gz → 0.7.4__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.
Files changed (38) hide show
  1. {xrtm_data-0.7.2/src/xrtm_data.egg-info → xrtm_data-0.7.4}/PKG-INFO +1 -1
  2. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/pyproject.toml +1 -1
  3. xrtm_data-0.7.4/src/xrtm/data/providers/online/_http.py +87 -0
  4. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/providers/online/kalshi.py +4 -10
  5. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/providers/online/manifold.py +4 -10
  6. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/providers/online/metaculus.py +4 -10
  7. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/providers/online/polymarket.py +5 -10
  8. {xrtm_data-0.7.2 → xrtm_data-0.7.4/src/xrtm_data.egg-info}/PKG-INFO +1 -1
  9. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm_data.egg-info/SOURCES.txt +1 -0
  10. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_online_sources.py +98 -0
  11. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/LICENSE +0 -0
  12. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/README.md +0 -0
  13. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/setup.cfg +0 -0
  14. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/__init__.py +0 -0
  15. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/__init__.py +0 -0
  16. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/interfaces.py +0 -0
  17. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/schemas/__init__.py +0 -0
  18. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/schemas/forecast.py +0 -0
  19. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/schemas/market.py +0 -0
  20. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/schemas/prior.py +0 -0
  21. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/schemas/resolution.py +0 -0
  22. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/core/schemas/trade.py +0 -0
  23. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/corpora/__init__.py +0 -0
  24. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/corpora/_builtin_corpora.py +0 -0
  25. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/corpora/real_binary.py +0 -0
  26. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/corpora/registry.py +0 -0
  27. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/providers/__init__.py +0 -0
  28. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/providers/online/__init__.py +0 -0
  29. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm/data/version.py +0 -0
  30. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm_data.egg-info/dependency_links.txt +0 -0
  31. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm_data.egg-info/requires.txt +0 -0
  32. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/src/xrtm_data.egg-info/top_level.txt +0 -0
  33. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_corpus_registry.py +0 -0
  34. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_market_snapshot.py +0 -0
  35. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_prior_schemas.py +0 -0
  36. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_real_binary_corpus.py +0 -0
  37. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_resolution_schemas.py +0 -0
  38. {xrtm_data-0.7.2 → xrtm_data-0.7.4}/tests/test_schemas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrtm-data
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: The Snapshot Vault for XRTM.
5
5
  Author-email: XRTM Team <moy@xrtm.org>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xrtm-data"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
8
  description = "The Snapshot Vault for XRTM."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.13"
@@ -0,0 +1,87 @@
1
+ # coding=utf-8
2
+ # Copyright 2026 XRTM Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ r"""Shared JSON fetching with retry/backoff for online providers.
17
+
18
+ A single transient network blip used to fail an entire collection pass; these
19
+ providers now retry idempotent GETs a bounded number of times.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import random
27
+ import time
28
+ import urllib.error
29
+ import urllib.request
30
+ from typing import Any, Dict, Optional
31
+
32
+ from xrtm.data.core import DataSourceError
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ __all__ = ["fetch_json"]
37
+
38
+ DEFAULT_USER_AGENT = "xrtm/0.1"
39
+ RETRYABLE_STATUS = frozenset({408, 409, 425, 429, 500, 502, 503, 504})
40
+
41
+
42
+ def fetch_json(
43
+ url: str,
44
+ *,
45
+ timeout: float = 15.0,
46
+ retries: int = 2,
47
+ headers: Optional[Dict[str, str]] = None,
48
+ provider: str = "API",
49
+ ) -> Any:
50
+ r"""GET *url* and parse JSON, retrying transient failures with backoff.
51
+
52
+ Client errors (4xx other than 408/409/425/429) are not retried — they will
53
+ fail again and retrying them just burns time.
54
+ """
55
+ request = urllib.request.Request(url, headers={"User-Agent": DEFAULT_USER_AGENT, **(headers or {})})
56
+ last_error: Optional[Exception] = None
57
+
58
+ for attempt in range(retries + 1):
59
+ try:
60
+ with urllib.request.urlopen(request, timeout=timeout) as response:
61
+ return json.loads(response.read().decode("utf-8"))
62
+ except urllib.error.HTTPError as exc:
63
+ last_error = exc
64
+ if exc.code not in RETRYABLE_STATUS or attempt >= retries:
65
+ raise DataSourceError(f"{provider} API error: {exc}") from exc
66
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
67
+ # The body was read in full but does not parse: deterministic, so
68
+ # retrying only multiplies the damage (e.g. an HTML maintenance
69
+ # page served with HTTP 200).
70
+ raise DataSourceError(f"{provider} API returned unparseable JSON: {exc}") from exc
71
+ except Exception as exc: # noqa: BLE001 - connection/timeout errors are transient
72
+ last_error = exc
73
+ if attempt >= retries:
74
+ raise DataSourceError(f"{provider} API error: {exc}") from exc
75
+
76
+ wait = (0.5 * (2**attempt)) + random.uniform(0.0, 0.25)
77
+ logger.warning(
78
+ "%s request failed (attempt %d/%d), retrying in %.1fs: %s",
79
+ provider,
80
+ attempt + 1,
81
+ retries,
82
+ wait,
83
+ last_error,
84
+ )
85
+ time.sleep(wait)
86
+
87
+ raise DataSourceError(f"{provider} API error: {last_error}") # pragma: no cover - loop raises
@@ -23,15 +23,14 @@ API docs: https://trading-api.readme.io/
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- import json
27
26
  import logging
28
- import urllib.request
29
27
  from datetime import datetime, timezone
30
28
  from typing import Any, List, Optional
31
29
 
32
- from xrtm.data.core.interfaces import DataSource, DataSourceError, ResolutionSource
30
+ from xrtm.data.core.interfaces import DataSource, ResolutionSource
33
31
  from xrtm.data.core.schemas.forecast import ForecastQuestion, MetadataBase
34
32
  from xrtm.data.core.schemas.resolution import ResolvedQuestion
33
+ from xrtm.data.providers.online._http import fetch_json
35
34
 
36
35
  logger = logging.getLogger(__name__)
37
36
 
@@ -148,13 +147,8 @@ class KalshiSource(DataSource, ResolutionSource):
148
147
  return resolved
149
148
 
150
149
  def _get_json(self, url: str) -> Any:
151
- r"""Fetch JSON from a URL."""
152
- req = urllib.request.Request(url, headers={"User-Agent": "xrtm/0.1"})
153
- try:
154
- with urllib.request.urlopen(req, timeout=20) as resp:
155
- return json.loads(resp.read().decode("utf-8"))
156
- except Exception as exc:
157
- raise DataSourceError(f"Kalshi API error: {exc}") from exc
150
+ r"""Fetch JSON from a URL (retry/backoff on transient failures)."""
151
+ return fetch_json(url, timeout=20, provider="Kalshi")
158
152
 
159
153
  @staticmethod
160
154
  def _to_forecast_question(item: dict[str, Any]) -> ForecastQuestion:
@@ -23,15 +23,14 @@ API docs: https://docs.manifold.markets/api
23
23
 
24
24
  from __future__ import annotations
25
25
 
26
- import json
27
26
  import logging
28
- import urllib.request
29
27
  from datetime import datetime, timezone
30
28
  from typing import Any, List, Optional
31
29
 
32
- from xrtm.data.core.interfaces import DataSource, DataSourceError, ResolutionSource
30
+ from xrtm.data.core.interfaces import DataSource, ResolutionSource
33
31
  from xrtm.data.core.schemas.forecast import ForecastQuestion, MetadataBase
34
32
  from xrtm.data.core.schemas.resolution import ResolvedQuestion
33
+ from xrtm.data.providers.online._http import fetch_json
35
34
 
36
35
  logger = logging.getLogger(__name__)
37
36
 
@@ -149,13 +148,8 @@ class ManifoldSource(DataSource, ResolutionSource):
149
148
  return resolved
150
149
 
151
150
  def _get_json(self, url: str) -> Any:
152
- r"""Fetch JSON from a URL."""
153
- req = urllib.request.Request(url, headers={"User-Agent": "xrtm/0.1"})
154
- try:
155
- with urllib.request.urlopen(req, timeout=20) as resp:
156
- return json.loads(resp.read().decode("utf-8"))
157
- except Exception as exc:
158
- raise DataSourceError(f"Manifold API error: {exc}") from exc
151
+ r"""Fetch JSON from a URL (retry/backoff on transient failures)."""
152
+ return fetch_json(url, timeout=20, provider="Manifold")
159
153
 
160
154
  @staticmethod
161
155
  def _to_forecast_question(item: dict[str, Any]) -> ForecastQuestion:
@@ -25,15 +25,14 @@ API docs: https://www.metaculus.com/api2/
25
25
 
26
26
  from __future__ import annotations
27
27
 
28
- import json
29
28
  import logging
30
- import urllib.request
31
29
  from datetime import datetime, timezone
32
30
  from typing import Any
33
31
 
34
32
  from xrtm.data.core.interfaces import DataSource, DataSourceError, ResolutionSource
35
33
  from xrtm.data.core.schemas.forecast import ForecastQuestion, MetadataBase
36
34
  from xrtm.data.core.schemas.resolution import ResolvedQuestion
35
+ from xrtm.data.providers.online._http import fetch_json
37
36
 
38
37
  logger = logging.getLogger(__name__)
39
38
 
@@ -198,16 +197,11 @@ class MetaculusSource(DataSource, ResolutionSource):
198
197
  return datetime.now(timezone.utc)
199
198
 
200
199
  def _get_json(self, url: str) -> dict[str, Any]:
201
- r"""Fetch JSON from a URL."""
202
- headers = {"User-Agent": "xrtm/0.1"}
200
+ r"""Fetch JSON from a URL (retry/backoff on transient failures)."""
201
+ headers: dict[str, str] = {}
203
202
  if self.api_key:
204
203
  headers["Authorization"] = f"Token {self.api_key}"
205
- req = urllib.request.Request(url, headers=headers)
206
- try:
207
- with urllib.request.urlopen(req, timeout=15) as resp:
208
- return json.loads(resp.read().decode("utf-8"))
209
- except Exception as exc:
210
- raise DataSourceError(f"Metaculus API error: {exc}") from exc
204
+ return fetch_json(url, timeout=15, headers=headers, provider="Metaculus")
211
205
 
212
206
  @staticmethod
213
207
  def _to_forecast_question(item: dict[str, Any]) -> ForecastQuestion:
@@ -25,13 +25,13 @@ from __future__ import annotations
25
25
 
26
26
  import json
27
27
  import logging
28
- import urllib.request
29
28
  from datetime import datetime, timezone
30
29
  from typing import Any
31
30
 
32
31
  from xrtm.data.core.interfaces import DataSource, DataSourceError, ResolutionSource
33
32
  from xrtm.data.core.schemas.forecast import ForecastQuestion, MetadataBase
34
33
  from xrtm.data.core.schemas.resolution import ResolvedQuestion
34
+ from xrtm.data.providers.online._http import fetch_json
35
35
 
36
36
  logger = logging.getLogger(__name__)
37
37
 
@@ -80,8 +80,8 @@ class PolymarketSource(DataSource, ResolutionSource):
80
80
  data = self._get_json(url)
81
81
  except DataSourceError:
82
82
  # Fallback: same markets endpoint without the volume ordering,
83
- # which some Gamma deployments reject.
84
- url = f"{self.api_base}/markets?limit={min(limit, 100)}&closed=false"
83
+ # which some Gamma deployments reject. Pagination still applies.
84
+ url = f"{self.api_base}/markets?limit={min(limit, 100)}&offset={offset}&closed=false"
85
85
  data = self._get_json(url)
86
86
 
87
87
  # Gamma API may wrap in "markets" key or return list directly
@@ -199,13 +199,8 @@ class PolymarketSource(DataSource, ResolutionSource):
199
199
  return datetime.now(timezone.utc)
200
200
 
201
201
  def _get_json(self, url: str) -> Any:
202
- r"""Fetch JSON from a URL."""
203
- req = urllib.request.Request(url, headers={"User-Agent": "xrtm/0.1"})
204
- try:
205
- with urllib.request.urlopen(req, timeout=15) as resp:
206
- return json.loads(resp.read().decode("utf-8"))
207
- except Exception as exc:
208
- raise DataSourceError(f"Polymarket API error: {exc}") from exc
202
+ r"""Fetch JSON from a URL (retry/backoff on transient failures)."""
203
+ return fetch_json(url, timeout=15, provider="Polymarket")
209
204
 
210
205
  @staticmethod
211
206
  def _to_forecast_question(item: dict[str, Any]) -> ForecastQuestion:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xrtm-data
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
4
  Summary: The Snapshot Vault for XRTM.
5
5
  Author-email: XRTM Team <moy@xrtm.org>
6
6
  License-Expression: Apache-2.0
@@ -17,6 +17,7 @@ src/xrtm/data/corpora/real_binary.py
17
17
  src/xrtm/data/corpora/registry.py
18
18
  src/xrtm/data/providers/__init__.py
19
19
  src/xrtm/data/providers/online/__init__.py
20
+ src/xrtm/data/providers/online/_http.py
20
21
  src/xrtm/data/providers/online/kalshi.py
21
22
  src/xrtm/data/providers/online/manifold.py
22
23
  src/xrtm/data/providers/online/metaculus.py
@@ -229,3 +229,101 @@ async def test_polymarket_fetch_questions_fallback_uses_markets(monkeypatch):
229
229
  assert len(calls["urls"]) == 2
230
230
  assert "/markets?" in calls["urls"][1]
231
231
  assert questions and questions[0].id == "polymarket-m1"
232
+
233
+
234
+ def test_fetch_json_retries_transient_failures(monkeypatch):
235
+ from xrtm.data.providers.online import _http
236
+
237
+ calls = {"n": 0}
238
+
239
+ class _Response:
240
+ def __enter__(self):
241
+ return self
242
+
243
+ def __exit__(self, *args):
244
+ return False
245
+
246
+ def read(self):
247
+ return b'{"ok": true}'
248
+
249
+ def _urlopen(request, timeout=None):
250
+ calls["n"] += 1
251
+ if calls["n"] < 3:
252
+ raise OSError("connection reset")
253
+ return _Response()
254
+
255
+ monkeypatch.setattr(_http.urllib.request, "urlopen", _urlopen)
256
+ monkeypatch.setattr(_http.time, "sleep", lambda _seconds: None)
257
+
258
+ assert _http.fetch_json("https://example.test/x") == {"ok": True}
259
+ assert calls["n"] == 3
260
+
261
+
262
+ def test_fetch_json_does_not_retry_client_errors(monkeypatch):
263
+ import urllib.error
264
+
265
+ from xrtm.data import DataSourceError
266
+ from xrtm.data.providers.online import _http
267
+
268
+ calls = {"n": 0}
269
+
270
+ def _urlopen(request, timeout=None):
271
+ calls["n"] += 1
272
+ raise urllib.error.HTTPError(request.full_url, 404, "Not Found", {}, None)
273
+
274
+ monkeypatch.setattr(_http.urllib.request, "urlopen", _urlopen)
275
+ monkeypatch.setattr(_http.time, "sleep", lambda _seconds: None)
276
+
277
+ with pytest.raises(DataSourceError):
278
+ _http.fetch_json("https://example.test/x")
279
+ assert calls["n"] == 1
280
+
281
+
282
+ def test_fetch_json_does_not_retry_unparseable_bodies(monkeypatch):
283
+ from xrtm.data.providers.online import _http
284
+
285
+ calls = {"n": 0}
286
+
287
+ class _Response:
288
+ def __enter__(self):
289
+ return self
290
+
291
+ def __exit__(self, *args):
292
+ return False
293
+
294
+ def read(self):
295
+ return b"<html>maintenance</html>"
296
+
297
+ def _urlopen(request, timeout=None):
298
+ calls["n"] += 1
299
+ return _Response()
300
+
301
+ monkeypatch.setattr(_http.urllib.request, "urlopen", _urlopen)
302
+ monkeypatch.setattr(_http.time, "sleep", lambda _seconds: None)
303
+
304
+ from xrtm.data import DataSourceError
305
+
306
+ with pytest.raises(DataSourceError, match="unparseable"):
307
+ _http.fetch_json("https://example.test/x")
308
+ assert calls["n"] == 1 # deterministic parse failure: no retries
309
+
310
+
311
+ @pytest.mark.asyncio
312
+ async def test_polymarket_fallback_keeps_offset(monkeypatch):
313
+ from xrtm.data.core import DataSourceError
314
+
315
+ source = PolymarketSource()
316
+ calls = {"urls": []}
317
+
318
+ def _get_json(url):
319
+ calls["urls"].append(url)
320
+ if "order=volume24hr" in url:
321
+ raise DataSourceError("volume ordering unsupported")
322
+ return []
323
+
324
+ monkeypatch.setattr(source, "_get_json", _get_json)
325
+
326
+ await source.fetch_questions(limit=5, offset=20)
327
+
328
+ assert len(calls["urls"]) == 2
329
+ assert "offset=20" in calls["urls"][1]
File without changes
File without changes
File without changes