tensorfeed 2.2.1__tar.gz → 3.0.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.2.1
3
+ Version: 3.0.0
4
4
  Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)
5
5
  Author-email: "TensorFeed.ai" <feedback@tensorfeed.ai>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tensorfeed"
7
- version = "2.2.1"
7
+ version = "3.0.0"
8
8
  description = "Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,4 +21,4 @@ __all__ = [
21
21
  "PaymentRequired",
22
22
  "RateLimited",
23
23
  ]
24
- __version__ = "2.2.0"
24
+ __version__ = "3.0.0"
@@ -18,7 +18,7 @@ from typing import Any # noqa: F401 (re-exported by purchase_credits return ty
18
18
 
19
19
 
20
20
  DEFAULT_BASE_URL = "https://tensorfeed.ai/api"
21
- DEFAULT_USER_AGENT = "TensorFeed-SDK-Python/2.2"
21
+ DEFAULT_USER_AGENT = "TensorFeed-SDK-Python/3.0"
22
22
 
23
23
 
24
24
  class TensorFeedError(Exception):
@@ -105,10 +105,18 @@ class TensorFeed:
105
105
  # Premium endpoints, balance, and explicit-require-token paths get
106
106
  # the Authorization header. Free endpoints do not advertise a token
107
107
  # (avoids accidentally leaking it to public-data endpoints).
108
+ # Paths handed to _request are RELATIVE: base_url already ends in /api,
109
+ # and url is built as f"{base_url}{path}". The old fallback tested for
110
+ # "/api/premium/" and "/api/payment/balance" in that relative path, so
111
+ # neither could ever match and the whole fallback was dead code. Every
112
+ # premium method happened to pass require_token=True explicitly, except
113
+ # attention_series, which therefore called a paid endpoint with no
114
+ # Authorization header at all and 402'd for customers holding credits.
115
+ # Matching the relative form makes the safety net real.
108
116
  needs_auth = (
109
117
  require_token
110
- or "/api/premium/" in path
111
- or path == "/api/payment/balance"
118
+ or path.startswith("/premium/")
119
+ or path == "/payment/balance"
112
120
  )
113
121
  if needs_auth and self.token:
114
122
  headers["Authorization"] = f"Bearer {self.token}"
@@ -380,7 +388,9 @@ class TensorFeed:
380
388
  kwargs["from"] = from_date
381
389
  if to_date is not None:
382
390
  kwargs["to"] = to_date
383
- return self._get("/premium/attention/series", **kwargs)
391
+ return self._request(
392
+ "GET", "/premium/attention/series", params=kwargs, require_token=True
393
+ )
384
394
 
385
395
  def agent_activity(self) -> dict[str, Any]:
386
396
  """Get agent traffic metrics. Free."""
@@ -510,7 +520,7 @@ class TensorFeed:
510
520
 
511
521
  For windows up to 90 days plus ``incident_count`` and
512
522
  ``mttr_minutes`` per provider, use ``status_leaderboard()``
513
- (premium, 1 credit).
523
+ (premium, 5 credits).
514
524
 
515
525
  Args:
516
526
  days: 1 to 7 (default 7).
@@ -1338,8 +1348,8 @@ class TensorFeed:
1338
1348
 
1339
1349
  Free, no auth. Single edge-cached endpoint that fans out across
1340
1350
  every daily TensorFeed feed (news, 3 paper feeds, HF
1341
- models/datasets/Spaces, hot GitHub issues, Reddit threads,
1342
- OpenRouter catalog summary, provider status) and returns a
1351
+ models/datasets/Spaces, hot GitHub issues, OpenRouter catalog
1352
+ summary, provider status) and returns a
1343
1353
  structured response. Saves an agent from orchestrating 9
1344
1354
  separate calls.
1345
1355
 
@@ -1455,31 +1465,6 @@ class TensorFeed:
1455
1465
  """
1456
1466
  return self._request("GET", "/openrouter/models")
1457
1467
 
1458
- def get_reddit_trending(self) -> dict[str, Any]:
1459
- """Currently-hot Reddit threads in 7 AI-relevant subreddits.
1460
-
1461
- Free, no auth. Subreddits queried: LocalLLaMA, MachineLearning,
1462
- ClaudeAI, OpenAI, singularity, artificial, AI_Agents. Pulls each
1463
- subreddit's "hot" listing, dedups by post id, drops stickied and
1464
- NSFW posts, ranks by score, top 30. Refreshed daily at 13:00 UTC.
1465
- Companion to ``get_hot_issues()``: that surfaces developer-side
1466
- conversation on GitHub, this surfaces community-side conversation
1467
- on Reddit.
1468
-
1469
- Titles pass through TensorFeed's prompt-injection sanitization at
1470
- capture time, so role-confusion tokens and bidi/zero-width
1471
- spoofing characters in Reddit posts cannot reach an agent.
1472
-
1473
- Returns:
1474
- Dict with ``ok`` and ``snapshot`` keys. Snapshot includes
1475
- ``date``, ``capturedAt``, ``total_posts``, ``subreddits_queried``,
1476
- ``posts`` (each with id, subreddit, title, author, score,
1477
- upvote_ratio, num_comments, permalink, url, created_utc,
1478
- flair, is_self, is_video), and ``summary`` (by_subreddit,
1479
- top_authors).
1480
- """
1481
- return self._request("GET", "/reddit/trending")
1482
-
1483
1468
  def get_hot_issues(self) -> dict[str, Any]:
1484
1469
  """Currently-hot GitHub issues across the AI ecosystem.
1485
1470
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfeed
3
- Version: 2.2.1
3
+ Version: 3.0.0
4
4
  Summary: Python SDK for the TensorFeed.ai API: AI news, status, model pricing, premium routing + history series + webhook watches, LangChain/CrewAI tool factories, agent payments via USDC on Base (optional web3 auto-send)
5
5
  Author-email: "TensorFeed.ai" <feedback@tensorfeed.ai>
6
6
  License-Expression: MIT
@@ -12,5 +12,6 @@ tensorfeed.egg-info/SOURCES.txt
12
12
  tensorfeed.egg-info/dependency_links.txt
13
13
  tensorfeed.egg-info/requires.txt
14
14
  tensorfeed.egg-info/top_level.txt
15
+ tests/test_premium_auth_header.py
15
16
  tests/test_premium_tools.py
16
17
  tests/test_route_verdict.py
@@ -0,0 +1,148 @@
1
+ """Every premium method must send the Authorization header.
2
+
3
+ A customer buys credits, calls a documented premium method, and gets a 402
4
+ because the SDK never attached their token. That shipped in 2.2.1:
5
+ ``attention_series`` was the only premium method of twenty that omitted
6
+ ``require_token=True``, and the fallback meant to catch exactly that mistake
7
+ was dead code.
8
+
9
+ The fallback tested ``"/api/premium/" in path``. Paths handed to ``_request``
10
+ are relative, because ``base_url`` already ends in ``/api`` and the URL is
11
+ built as ``f"{base_url}{path}"``. So the substring could never match and the
12
+ safety net never fired for anything.
13
+
14
+ Two guards here, because either one alone is escapable:
15
+
16
+ 1. Behavioral. Drive every premium method through a stub transport and assert
17
+ the header is actually present on the wire. Catches a missing flag no
18
+ matter how the method is written.
19
+ 2. Static. Assert the fallback still matches the relative path form, so the
20
+ net cannot silently go dead again while every method happens to pass the
21
+ flag explicitly.
22
+
23
+ Network-free. Run from sdk/python::
24
+
25
+ python -m unittest discover -s tests -t .
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import inspect
31
+ import json
32
+ import unittest
33
+ from unittest import mock
34
+
35
+ from tensorfeed import TensorFeed
36
+ from tensorfeed import client as client_mod
37
+
38
+ TOKEN = "tf_live_testtoken0000000000"
39
+
40
+
41
+ class _StubResponse:
42
+ """Minimal stand-in for the urlopen context manager."""
43
+
44
+ status = 200
45
+ headers: dict[str, str] = {}
46
+
47
+ def read(self) -> bytes:
48
+ return json.dumps({"ok": True}).encode()
49
+
50
+ def __enter__(self):
51
+ return self
52
+
53
+ def __exit__(self, *exc):
54
+ return False
55
+
56
+
57
+ def _premium_methods():
58
+ """Public methods whose source calls a /premium/ path."""
59
+ for name, fn in inspect.getmembers(TensorFeed, predicate=inspect.isfunction):
60
+ if name.startswith("_"):
61
+ continue
62
+ try:
63
+ src = inspect.getsource(fn)
64
+ except OSError: # pragma: no cover
65
+ continue
66
+ if '"/premium/' in src or "'/premium/" in src:
67
+ yield name, fn
68
+
69
+
70
+ class PremiumMethodsSendTheToken(unittest.TestCase):
71
+ def test_at_least_a_dozen_premium_methods_are_discovered(self):
72
+ found = list(_premium_methods())
73
+ self.assertGreater(
74
+ len(found), 12, f"expected to discover many premium methods, got {len(found)}"
75
+ )
76
+
77
+ def test_every_premium_method_attaches_authorization(self):
78
+ tf = TensorFeed(token=TOKEN)
79
+ offenders: list[str] = []
80
+
81
+ for name, fn in _premium_methods():
82
+ sig = inspect.signature(fn)
83
+ # Supply a placeholder for every required positional parameter so the
84
+ # method reaches the transport. Values are irrelevant: the stub
85
+ # answers everything with 200 and we only inspect the request.
86
+ args: list[object] = []
87
+ kwargs: dict[str, object] = {}
88
+ for pname, p in list(sig.parameters.items())[1:]:
89
+ if p.default is not inspect.Parameter.empty:
90
+ continue
91
+ if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
92
+ continue
93
+ # Numeric-sounding params need a number; everything else takes a
94
+ # plausible string. The value never reaches a real service.
95
+ filler: object = 1000 if "tokens" in pname or pname.endswith("_per_day") else "anthropic"
96
+ if p.kind is p.KEYWORD_ONLY:
97
+ kwargs[pname] = filler
98
+ else:
99
+ args.append(filler)
100
+
101
+ captured: dict[str, object] = {}
102
+
103
+ def _fake_urlopen(req, *a, **kw):
104
+ captured["headers"] = {k.lower(): v for k, v in req.headers.items()}
105
+ captured["url"] = req.full_url
106
+ return _StubResponse()
107
+
108
+ with mock.patch.object(client_mod.urllib.request, "urlopen", _fake_urlopen):
109
+ try:
110
+ getattr(tf, name)(*args, **kwargs)
111
+ except Exception as exc: # noqa: BLE001
112
+ offenders.append(f"{name}: raised before transport ({exc!r})")
113
+ continue
114
+
115
+ headers = captured.get("headers") or {}
116
+ if headers.get("authorization") != f"Bearer {TOKEN}":
117
+ offenders.append(
118
+ f"{name}: no Authorization header on {captured.get('url')}"
119
+ )
120
+
121
+ self.assertEqual(
122
+ offenders,
123
+ [],
124
+ "These premium methods call a paid endpoint without the customer's token, so a "
125
+ "customer holding credits gets a 402 instead of data:\n " + "\n ".join(offenders),
126
+ )
127
+
128
+
129
+ class TheAuthFallbackIsNotDeadCode(unittest.TestCase):
130
+ def test_fallback_matches_the_relative_path_form(self):
131
+ src = inspect.getsource(client_mod.TensorFeed._request)
132
+ self.assertIn(
133
+ 'path.startswith("/premium/")',
134
+ src,
135
+ "The premium auth fallback must match the RELATIVE path form. base_url already "
136
+ "ends in /api, so a check for '/api/premium/' in path can never be true and the "
137
+ "safety net is dead code.",
138
+ )
139
+ self.assertNotIn(
140
+ '"/api/premium/" in path',
141
+ src,
142
+ "This is the dead check that let attention_series ship without a token.",
143
+ )
144
+
145
+ def test_base_url_still_ends_in_api(self):
146
+ # The premise of the test above. If this ever changes, the relative-path
147
+ # assumption changes with it.
148
+ self.assertTrue(client_mod.DEFAULT_BASE_URL.endswith("/api"))
File without changes
File without changes