Skip to content

Commit 2ae66b5

Browse files
Mlaz-codeclaude
andcommitted
feat(auth): support Bearer token alongside X-API-Key
Add an optional ``auth_method`` keyword on the ``SharpAPI`` and ``AsyncSharpAPI`` constructors. Defaults to ``"x-api-key"`` (existing behaviour — fully back-compat; no caller changes required). When set to ``"bearer"`` the SDK sends ``Authorization: Bearer <key>`` instead of the ``X-API-Key`` header, matching the Go server's three accepted REST auth modes (header, Bearer, query). Why: customers running behind IAM layers, SSO gateways, or corporate proxies often have non-standard headers stripped or rewritten. Standard ``Authorization: Bearer`` survives those hops and integrates cleanly with off-the-shelf auth middleware. SSE streams are intentionally unchanged — they always authenticate via the ``?api_key=`` query param because the EventSource spec does not allow custom request headers. Bumps version 0.2.4 -> 0.2.5 (new public API surface). Adds 2 tests (sync + async) verifying the Bearer header is sent and X-API-Key is omitted in bearer mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b5a5e57 commit 2ae66b5

7 files changed

Lines changed: 94 additions & 9 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "sharpapi"
7-
version = "0.2.4"
7+
version = "0.2.5"
88
description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
99
readme = "README.md"
1010
license = "MIT"

src/sharpapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
)
5858
from .streaming import EventStream
5959

60-
__version__ = "0.2.4"
60+
__version__ = "0.2.5"
6161

6262
__all__ = [
6363
# Clients

src/sharpapi/_base.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import random
6+
from typing import Literal
67

78
import httpx
89

@@ -19,7 +20,12 @@
1920

2021
DEFAULT_BASE_URL = "https://api.sharpapi.io"
2122
DEFAULT_TIMEOUT = 30.0
22-
USER_AGENT = "sharpapi-python/0.2.4"
23+
USER_AGENT = "sharpapi-python/0.2.5"
24+
25+
# Supported REST authentication methods. SSE always uses ``?api_key=`` query
26+
# regardless of this setting because EventSource cannot set custom headers.
27+
AuthMethod = Literal["x-api-key", "bearer"]
28+
DEFAULT_AUTH_METHOD: AuthMethod = "x-api-key"
2329

2430
RETRY_STATUSES = frozenset({502, 503, 504})
2531
RETRY_MAX_ATTEMPTS = 3
@@ -138,13 +144,28 @@ def handle_errors(response: httpx.Response) -> None:
138144
raise SharpAPIError(error_msg, code=code, status=status)
139145

140146

141-
def make_headers(api_key: str) -> dict[str, str]:
142-
"""Build default request headers."""
143-
return {
144-
"X-API-Key": api_key,
147+
def make_headers(
148+
api_key: str,
149+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
150+
) -> dict[str, str]:
151+
"""Build default request headers.
152+
153+
Args:
154+
api_key: The SharpAPI key (e.g. ``sk_live_...``).
155+
auth_method: Either ``"x-api-key"`` (default — sends an
156+
``X-API-Key`` header) or ``"bearer"`` (sends
157+
``Authorization: Bearer <key>``). Useful when proxies, IAM
158+
layers, or SSO gateways strip non-standard custom headers.
159+
"""
160+
headers: dict[str, str] = {
145161
"Content-Type": "application/json",
146162
"User-Agent": USER_AGENT,
147163
}
164+
if auth_method == "bearer":
165+
headers["Authorization"] = f"Bearer {api_key}"
166+
else:
167+
headers["X-API-Key"] = api_key
168+
return headers
148169

149170

150171
def _int_or_none(value: str | None) -> int | None:

src/sharpapi/async_client.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import httpx
99

1010
from ._base import (
11+
DEFAULT_AUTH_METHOD,
1112
DEFAULT_BASE_URL,
1213
DEFAULT_TIMEOUT,
1314
RETRY_MAX_ATTEMPTS,
15+
AuthMethod,
1416
handle_errors,
1517
make_headers,
1618
parse_rate_limit,
@@ -44,6 +46,17 @@ class AsyncSharpAPI:
4446
Provides typed access to odds, +EV, arbitrage, middles, and streaming
4547
endpoints using ``async``/``await``.
4648
49+
Args:
50+
api_key: Your SharpAPI key (e.g. ``sk_live_...``).
51+
base_url: Override the API base URL (defaults to production).
52+
timeout: HTTP timeout in seconds.
53+
auth_method: How to send the API key on REST requests. ``"x-api-key"``
54+
(default) sends the ``X-API-Key`` header. ``"bearer"`` sends
55+
``Authorization: Bearer <key>`` instead — useful when running
56+
behind IAM layers, SSO, or API gateways that strip custom
57+
headers. SSE streams (sync client only) always authenticate via
58+
``?api_key=`` query and are unaffected.
59+
4760
Example::
4861
4962
import asyncio
@@ -55,6 +68,10 @@ async def main():
5568
for arb in arbs.data:
5669
print(f"{arb.profit_percent}% — {arb.event_name}")
5770
71+
# Or, behind a proxy that requires standard Bearer auth:
72+
async with AsyncSharpAPI("sk_live_xxx", auth_method="bearer") as c:
73+
...
74+
5875
asyncio.run(main())
5976
"""
6077

@@ -64,16 +81,18 @@ def __init__(
6481
*,
6582
base_url: str = DEFAULT_BASE_URL,
6683
timeout: float = DEFAULT_TIMEOUT,
84+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
6785
):
6886
if not api_key:
6987
raise ValueError("api_key is required")
7088

7189
self._api_key = api_key
90+
self._auth_method: AuthMethod = auth_method
7291
self._base_url = base_url.rstrip("/")
7392
self._timeout = timeout
7493
self._http = httpx.AsyncClient(
7594
base_url=f"{self._base_url}/api/v1",
76-
headers=make_headers(api_key),
95+
headers=make_headers(api_key, auth_method),
7796
timeout=timeout,
7897
)
7998
self._last_rate_limit = RateLimitInfo()

src/sharpapi/client.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import httpx
99

1010
from ._base import (
11+
DEFAULT_AUTH_METHOD,
1112
DEFAULT_BASE_URL,
1213
DEFAULT_TIMEOUT,
1314
RETRY_MAX_ATTEMPTS,
15+
AuthMethod,
1416
handle_errors,
1517
make_headers,
1618
parse_rate_limit,
@@ -45,12 +47,26 @@ class SharpAPI:
4547
Provides typed access to odds, +EV, arbitrage, middles, and streaming
4648
endpoints.
4749
50+
Args:
51+
api_key: Your SharpAPI key (e.g. ``sk_live_...``).
52+
base_url: Override the API base URL (defaults to production).
53+
timeout: HTTP timeout in seconds.
54+
auth_method: How to send the API key on REST requests. ``"x-api-key"``
55+
(default) sends the ``X-API-Key`` header. ``"bearer"`` sends
56+
``Authorization: Bearer <key>`` instead — useful when running
57+
behind IAM layers, SSO, or API gateways that strip custom
58+
headers. SSE streams always authenticate via ``?api_key=`` query
59+
(EventSource cannot set headers) and are unaffected.
60+
4861
Example::
4962
5063
from sharpapi import SharpAPI
5164
5265
client = SharpAPI("sk_live_xxx")
5366
67+
# Or, behind a proxy that requires standard Bearer auth:
68+
client = SharpAPI("sk_live_xxx", auth_method="bearer")
69+
5470
# Get arbitrage opportunities
5571
arbs = client.arbitrage.get(min_profit=1.0)
5672
for arb in arbs.data:
@@ -73,16 +89,18 @@ def __init__(
7389
*,
7490
base_url: str = DEFAULT_BASE_URL,
7591
timeout: float = DEFAULT_TIMEOUT,
92+
auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
7693
):
7794
if not api_key:
7895
raise ValueError("api_key is required")
7996

8097
self._api_key = api_key
98+
self._auth_method: AuthMethod = auth_method
8199
self._base_url = base_url.rstrip("/")
82100
self._timeout = timeout
83101
self._http = httpx.Client(
84102
base_url=f"{self._base_url}/api/v1",
85-
headers=make_headers(api_key),
103+
headers=make_headers(api_key, auth_method),
86104
timeout=timeout,
87105
)
88106
self._last_rate_limit = RateLimitInfo()

tests/test_async_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,16 @@ async def test_api_key_header_sent(self):
223223
async with AsyncSharpAPI(API_KEY) as client:
224224
await client.sports.list()
225225
assert route.calls[0].request.headers["x-api-key"] == API_KEY
226+
assert "authorization" not in route.calls[0].request.headers
227+
228+
@pytest.mark.asyncio
229+
@respx.mock
230+
async def test_bearer_auth_method_sends_authorization(self):
231+
route = respx.get(f"{BASE_URL}/api/v1/sports").mock(
232+
return_value=Response(200, json=SPORTS_RESPONSE)
233+
)
234+
async with AsyncSharpAPI(API_KEY, auth_method="bearer") as client:
235+
await client.sports.list()
236+
req = route.calls[0].request
237+
assert req.headers["authorization"] == f"Bearer {API_KEY}"
238+
assert "x-api-key" not in req.headers

tests/test_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,20 @@ def test_api_key_header_sent(self):
382382
with SharpAPI(API_KEY) as client:
383383
client.sports.list()
384384
assert route.calls[0].request.headers["x-api-key"] == API_KEY
385+
# Bearer must NOT be sent in the default mode.
386+
assert "authorization" not in route.calls[0].request.headers
387+
388+
@respx.mock
389+
def test_bearer_auth_method_sends_authorization(self):
390+
route = respx.get(f"{BASE_URL}/api/v1/sports").mock(
391+
return_value=Response(200, json=SPORTS_RESPONSE)
392+
)
393+
with SharpAPI(API_KEY, auth_method="bearer") as client:
394+
client.sports.list()
395+
req = route.calls[0].request
396+
assert req.headers["authorization"] == f"Bearer {API_KEY}"
397+
# X-API-Key must NOT be sent in bearer mode.
398+
assert "x-api-key" not in req.headers
385399

386400
@respx.mock
387401
def test_user_agent_sent(self):

0 commit comments

Comments
 (0)