tradezero-sdk 1.0.0__py3-none-any.whl
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.
- tradezero/__init__.py +35 -0
- tradezero/client/__init__.py +11 -0
- tradezero/client/async_client.py +89 -0
- tradezero/client/sync_client.py +93 -0
- tradezero/config.py +31 -0
- tradezero/enums.py +80 -0
- tradezero/exceptions.py +105 -0
- tradezero/http/__init__.py +9 -0
- tradezero/http/_base.py +35 -0
- tradezero/http/_retry.py +69 -0
- tradezero/http/async_http.py +144 -0
- tradezero/http/sync_http.py +145 -0
- tradezero/models/__init__.py +35 -0
- tradezero/models/accounts.py +90 -0
- tradezero/models/locates.py +98 -0
- tradezero/models/orders.py +144 -0
- tradezero/models/positions.py +43 -0
- tradezero/modules/__init__.py +21 -0
- tradezero/modules/accounts.py +89 -0
- tradezero/modules/locates.py +227 -0
- tradezero/modules/positions.py +65 -0
- tradezero/modules/trading.py +298 -0
- tradezero_sdk-1.0.0.dist-info/METADATA +441 -0
- tradezero_sdk-1.0.0.dist-info/RECORD +26 -0
- tradezero_sdk-1.0.0.dist-info/WHEEL +4 -0
- tradezero_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
tradezero/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""TradeZero SDK public interface."""
|
|
2
|
+
|
|
3
|
+
from tradezero.client.async_client import AsyncTradeZeroClient
|
|
4
|
+
from tradezero.client.sync_client import TradeZeroClient
|
|
5
|
+
from tradezero.enums import SecurityType
|
|
6
|
+
from tradezero.exceptions import (
|
|
7
|
+
APIValidationError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
ForbiddenError,
|
|
10
|
+
InsufficientFundsError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ServerError,
|
|
14
|
+
TradeZeroAPIError,
|
|
15
|
+
TradeZeroSDKError,
|
|
16
|
+
)
|
|
17
|
+
from tradezero.models.orders import TradeRecord
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"TradeZeroClient",
|
|
21
|
+
"AsyncTradeZeroClient",
|
|
22
|
+
"SecurityType",
|
|
23
|
+
"TradeZeroSDKError",
|
|
24
|
+
"TradeZeroAPIError",
|
|
25
|
+
"AuthenticationError",
|
|
26
|
+
"ForbiddenError",
|
|
27
|
+
"RateLimitError",
|
|
28
|
+
"APIValidationError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"InsufficientFundsError",
|
|
31
|
+
"ServerError",
|
|
32
|
+
"TradeRecord",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""High-level client façades.
|
|
2
|
+
|
|
3
|
+
Import from ``tradezero`` directly for normal usage::
|
|
4
|
+
|
|
5
|
+
from tradezero import TradeZeroClient, AsyncTradeZeroClient
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tradezero.client.async_client import AsyncTradeZeroClient
|
|
9
|
+
from tradezero.client.sync_client import TradeZeroClient
|
|
10
|
+
|
|
11
|
+
__all__ = ["TradeZeroClient", "AsyncTradeZeroClient"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Asynchronous top-level TradeZero client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from tradezero.config import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
base_url_from_env,
|
|
10
|
+
api_key_from_env,
|
|
11
|
+
api_secret_from_env,
|
|
12
|
+
)
|
|
13
|
+
from tradezero.http.async_http import AsyncHTTPClient
|
|
14
|
+
from tradezero.modules.accounts import AsyncAccountsModule
|
|
15
|
+
from tradezero.modules.trading import AsyncTradingModule
|
|
16
|
+
from tradezero.modules.positions import AsyncPositionsModule
|
|
17
|
+
from tradezero.modules.locates import AsyncLocatesModule
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncTradeZeroClient:
|
|
21
|
+
"""Asynchronous entry-point for the TradeZero SDK.
|
|
22
|
+
|
|
23
|
+
Designed to be used as an async context manager::
|
|
24
|
+
|
|
25
|
+
async with AsyncTradeZeroClient(api_key="...", api_secret="...") as client:
|
|
26
|
+
positions = await client.positions.get_positions("ACC123")
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
api_key: TradeZero API key ID. Falls back to ``TZ_API_KEY``.
|
|
30
|
+
api_secret: TradeZero API secret key. Falls back to ``TZ_API_SECRET``.
|
|
31
|
+
base_url: Override the default base URL. Falls back to ``TZ_BASE_URL``
|
|
32
|
+
or ``https://webapi.tradezero.com/v1/api``.
|
|
33
|
+
timeout: Per-request timeout in seconds (default: 30).
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
accounts: :class:`~tradezero.modules.accounts.AsyncAccountsModule`
|
|
37
|
+
trading: :class:`~tradezero.modules.trading.AsyncTradingModule`
|
|
38
|
+
positions: :class:`~tradezero.modules.positions.AsyncPositionsModule`
|
|
39
|
+
locates: :class:`~tradezero.modules.locates.AsyncLocatesModule`
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If credentials are missing from both kwargs and env vars.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
api_key: str | None = None,
|
|
49
|
+
api_secret: str | None = None,
|
|
50
|
+
base_url: str | None = None,
|
|
51
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
52
|
+
) -> None:
|
|
53
|
+
resolved_key = api_key or api_key_from_env()
|
|
54
|
+
resolved_secret = api_secret or api_secret_from_env()
|
|
55
|
+
resolved_url = base_url or base_url_from_env()
|
|
56
|
+
|
|
57
|
+
if not resolved_key:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"api_key is required. Pass it explicitly or set the TZ_API_KEY "
|
|
60
|
+
"environment variable."
|
|
61
|
+
)
|
|
62
|
+
if not resolved_secret:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"api_secret is required. Pass it explicitly or set the TZ_API_SECRET "
|
|
65
|
+
"environment variable."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self._http = AsyncHTTPClient(
|
|
69
|
+
api_key=resolved_key,
|
|
70
|
+
api_secret=resolved_secret,
|
|
71
|
+
base_url=resolved_url,
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Domain modules
|
|
76
|
+
self.accounts = AsyncAccountsModule(self._http)
|
|
77
|
+
self.trading = AsyncTradingModule(self._http)
|
|
78
|
+
self.positions = AsyncPositionsModule(self._http)
|
|
79
|
+
self.locates = AsyncLocatesModule(self._http)
|
|
80
|
+
|
|
81
|
+
async def aclose(self) -> None:
|
|
82
|
+
"""Release the underlying async HTTP connection pool."""
|
|
83
|
+
await self._http.aclose()
|
|
84
|
+
|
|
85
|
+
async def __aenter__(self) -> "AsyncTradeZeroClient":
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
89
|
+
await self.aclose()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Synchronous top-level TradeZero client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tradezero.config import (
|
|
9
|
+
DEFAULT_TIMEOUT,
|
|
10
|
+
base_url_from_env,
|
|
11
|
+
api_key_from_env,
|
|
12
|
+
api_secret_from_env,
|
|
13
|
+
)
|
|
14
|
+
from tradezero.http.sync_http import SyncHTTPClient
|
|
15
|
+
from tradezero.modules.accounts import AccountsModule
|
|
16
|
+
from tradezero.modules.trading import TradingModule
|
|
17
|
+
from tradezero.modules.positions import PositionsModule
|
|
18
|
+
from tradezero.modules.locates import LocatesModule
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TradeZeroClient:
|
|
22
|
+
"""Synchronous entry-point for the TradeZero SDK.
|
|
23
|
+
|
|
24
|
+
Instantiate once and reuse across calls. Supports context-manager usage
|
|
25
|
+
to automatically release HTTP connections::
|
|
26
|
+
|
|
27
|
+
with TradeZeroClient(api_key="...", api_secret="...") as client:
|
|
28
|
+
accounts = client.accounts.list_accounts()
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
api_key: TradeZero API key ID. Falls back to the ``TZ_API_KEY``
|
|
32
|
+
environment variable if omitted.
|
|
33
|
+
api_secret: TradeZero API secret key. Falls back to ``TZ_API_SECRET``.
|
|
34
|
+
base_url: Override the default base URL. Falls back to ``TZ_BASE_URL``
|
|
35
|
+
or ``https://webapi.tradezero.com/v1/api``.
|
|
36
|
+
timeout: Per-request timeout in seconds (default: 30).
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
accounts: :class:`~tradezero.modules.accounts.AccountsModule`
|
|
40
|
+
trading: :class:`~tradezero.modules.trading.TradingModule`
|
|
41
|
+
positions: :class:`~tradezero.modules.positions.PositionsModule`
|
|
42
|
+
locates: :class:`~tradezero.modules.locates.LocatesModule`
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If neither keyword arguments nor environment variables
|
|
46
|
+
supply the required credentials.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
api_key: str | None = None,
|
|
53
|
+
api_secret: str | None = None,
|
|
54
|
+
base_url: str | None = None,
|
|
55
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
56
|
+
) -> None:
|
|
57
|
+
resolved_key = api_key or api_key_from_env()
|
|
58
|
+
resolved_secret = api_secret or api_secret_from_env()
|
|
59
|
+
resolved_url = base_url or base_url_from_env()
|
|
60
|
+
|
|
61
|
+
if not resolved_key:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"api_key is required. Pass it explicitly or set the TZ_API_KEY "
|
|
64
|
+
"environment variable."
|
|
65
|
+
)
|
|
66
|
+
if not resolved_secret:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"api_secret is required. Pass it explicitly or set the TZ_API_SECRET "
|
|
69
|
+
"environment variable."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self._http = SyncHTTPClient(
|
|
73
|
+
api_key=resolved_key,
|
|
74
|
+
api_secret=resolved_secret,
|
|
75
|
+
base_url=resolved_url,
|
|
76
|
+
timeout=timeout,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Domain modules
|
|
80
|
+
self.accounts = AccountsModule(self._http)
|
|
81
|
+
self.trading = TradingModule(self._http)
|
|
82
|
+
self.positions = PositionsModule(self._http)
|
|
83
|
+
self.locates = LocatesModule(self._http)
|
|
84
|
+
|
|
85
|
+
def close(self) -> None:
|
|
86
|
+
"""Release the underlying HTTP connection pool."""
|
|
87
|
+
self._http.close()
|
|
88
|
+
|
|
89
|
+
def __enter__(self) -> "TradeZeroClient":
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def __exit__(self, *_: Any) -> None:
|
|
93
|
+
self.close()
|
tradezero/config.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""SDK configuration and constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL: str = "https://webapi.tradezero.com/v1/api"
|
|
8
|
+
|
|
9
|
+
#: Maximum number of retry attempts for transient errors.
|
|
10
|
+
DEFAULT_MAX_RETRIES: int = 3
|
|
11
|
+
|
|
12
|
+
#: Seconds to wait before the first retry.
|
|
13
|
+
DEFAULT_RETRY_WAIT_SECONDS: float = 1.0
|
|
14
|
+
|
|
15
|
+
#: HTTP timeout in seconds.
|
|
16
|
+
DEFAULT_TIMEOUT: float = 30.0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def base_url_from_env() -> str:
|
|
20
|
+
"""Return the base URL, preferring the ``TZ_BASE_URL`` environment variable."""
|
|
21
|
+
return os.environ.get("TZ_BASE_URL", DEFAULT_BASE_URL)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def api_key_from_env() -> str | None:
|
|
25
|
+
"""Return the API key from ``TZ_API_KEY`` or ``None``."""
|
|
26
|
+
return os.environ.get("TZ_API_KEY")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def api_secret_from_env() -> str | None:
|
|
30
|
+
"""Return the API secret from ``TZ_API_SECRET`` or ``None``."""
|
|
31
|
+
return os.environ.get("TZ_API_SECRET")
|
tradezero/enums.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Enumerations used across the TradeZero API."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ── Order enums ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OrderSide(str, Enum):
|
|
10
|
+
"""Direction of a trade order.
|
|
11
|
+
|
|
12
|
+
Values match the TradeZero API wire format exactly.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
BUY = "Buy"
|
|
16
|
+
SELL = "Sell"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OrderType(str, Enum):
|
|
20
|
+
"""Execution style of a trade order.
|
|
21
|
+
|
|
22
|
+
Values match the TradeZero API wire format exactly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
MARKET = "Market"
|
|
26
|
+
LIMIT = "Limit"
|
|
27
|
+
STOP = "Stop"
|
|
28
|
+
STOP_LIMIT = "StopLimit"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TimeInForce(str, Enum):
|
|
32
|
+
"""Time-in-force instructions for a trade order.
|
|
33
|
+
|
|
34
|
+
Values match the TradeZero API wire format exactly.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
DAY = "Day"
|
|
38
|
+
GTC = "GoodTillCancel"
|
|
39
|
+
IOC = "ImmediateOrCancel"
|
|
40
|
+
FOK = "FillOrKill"
|
|
41
|
+
AT_THE_OPENING = "AtTheOpening"
|
|
42
|
+
GOOD_TILL_CROSSING = "GoodTillCrossing"
|
|
43
|
+
DAY_PLUS = "Day_Plus"
|
|
44
|
+
GTC_PLUS = "GTC_Plus"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SecurityType(str, Enum):
|
|
48
|
+
"""Type of security for an order.
|
|
49
|
+
|
|
50
|
+
Values match the TradeZero API wire format exactly.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
STOCK = "Stock"
|
|
54
|
+
OPTION = "Option"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Locate enums ──────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LocateStatus(int, Enum):
|
|
61
|
+
"""Integer status codes for locate requests (FIX-style)."""
|
|
62
|
+
|
|
63
|
+
NEW = 48
|
|
64
|
+
FILLED = 50
|
|
65
|
+
CANCELED = 52
|
|
66
|
+
PENDING = 54
|
|
67
|
+
REJECTED = 56
|
|
68
|
+
OFFERED = 65
|
|
69
|
+
EXPIRED = 67
|
|
70
|
+
QUOTING = 81
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LocateTypeStr(str, Enum):
|
|
74
|
+
"""Human-readable locate type strings."""
|
|
75
|
+
|
|
76
|
+
UNKNOWN = "Unknown"
|
|
77
|
+
LOCATE = "Locate"
|
|
78
|
+
INTRADAY = "IntraDay"
|
|
79
|
+
PRE_BORROW = "PreBorrow"
|
|
80
|
+
SINGLE_USE = "SingleUse"
|
tradezero/exceptions.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Custom exception hierarchy for the TradeZero SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TradeZeroSDKError(Exception):
|
|
9
|
+
"""Base exception for all SDK errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TradeZeroAPIError(TradeZeroSDKError):
|
|
13
|
+
"""Raised when the TradeZero API returns a non-2xx response.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
status_code: HTTP status code returned by the API.
|
|
17
|
+
response_body: Raw response body text.
|
|
18
|
+
detail: Human-readable error detail extracted from the response.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
*,
|
|
25
|
+
status_code: int | None = None,
|
|
26
|
+
response_body: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.response_body = response_body
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
33
|
+
return (
|
|
34
|
+
f"{type(self).__name__}(status_code={self.status_code!r}, "
|
|
35
|
+
f"message={str(self)!r})"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthenticationError(TradeZeroAPIError):
|
|
40
|
+
"""Raised on 401 Unauthorized responses."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ForbiddenError(TradeZeroAPIError):
|
|
44
|
+
"""Raised on 403 Forbidden responses."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NotFoundError(TradeZeroAPIError):
|
|
48
|
+
"""Raised on 404 Not Found responses."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RateLimitError(TradeZeroAPIError):
|
|
52
|
+
"""Raised on 429 Too Many Requests responses."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class APIValidationError(TradeZeroAPIError):
|
|
56
|
+
"""Raised on 422 Unprocessable Entity (invalid request payload)."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class InsufficientFundsError(TradeZeroAPIError):
|
|
60
|
+
"""Raised when the account has insufficient buying power."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ServerError(TradeZeroAPIError):
|
|
64
|
+
"""Raised on 5xx server-side errors."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Status-code → exception mapping ──────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
_STATUS_MAP: dict[int, type[TradeZeroAPIError]] = {
|
|
70
|
+
401: AuthenticationError,
|
|
71
|
+
403: ForbiddenError,
|
|
72
|
+
404: NotFoundError,
|
|
73
|
+
422: APIValidationError,
|
|
74
|
+
429: RateLimitError,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def raise_for_status(status_code: int, body: str, url: str) -> None:
|
|
79
|
+
"""Map an HTTP status code to the appropriate SDK exception and raise it.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
status_code: HTTP status code from the API response.
|
|
83
|
+
body: Raw response body text for debugging.
|
|
84
|
+
url: Request URL for context in the error message.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
TradeZeroAPIError: Always raised with the most specific subclass available.
|
|
88
|
+
"""
|
|
89
|
+
exc_cls = _STATUS_MAP.get(status_code)
|
|
90
|
+
if exc_cls is None:
|
|
91
|
+
exc_cls = ServerError if status_code >= 500 else TradeZeroAPIError
|
|
92
|
+
|
|
93
|
+
# Attempt to surface a friendly message from the body
|
|
94
|
+
try:
|
|
95
|
+
import json
|
|
96
|
+
data: Any = json.loads(body)
|
|
97
|
+
detail = data.get("message") or data.get("detail") or data.get("error") or body
|
|
98
|
+
except Exception:
|
|
99
|
+
detail = body or "No detail provided."
|
|
100
|
+
|
|
101
|
+
raise exc_cls(
|
|
102
|
+
f"[{status_code}] {url} — {detail}",
|
|
103
|
+
status_code=status_code,
|
|
104
|
+
response_body=body,
|
|
105
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Low-level HTTP transport layer.
|
|
2
|
+
|
|
3
|
+
Internal implementation detail — prefer importing from ``tradezero`` directly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from tradezero.http.async_http import AsyncHTTPClient
|
|
7
|
+
from tradezero.http.sync_http import SyncHTTPClient
|
|
8
|
+
|
|
9
|
+
__all__ = ["SyncHTTPClient", "AsyncHTTPClient"]
|
tradezero/http/_base.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Shared utilities and header-building logic for HTTP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_auth_headers(api_key: str, api_secret: str) -> dict[str, str]:
|
|
9
|
+
"""Return the authentication headers required by every TradeZero API request.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
api_key: The ``TZ-API-KEY-ID`` value.
|
|
13
|
+
api_secret: The ``TZ-API-SECRET-KEY`` value.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A dictionary of HTTP headers to inject into every request.
|
|
17
|
+
"""
|
|
18
|
+
return {
|
|
19
|
+
"TZ-API-KEY-ID": api_key,
|
|
20
|
+
"TZ-API-SECRET-KEY": api_secret,
|
|
21
|
+
"Accept": "application/json",
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def strip_none(params: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
"""Remove keys whose value is ``None`` from a query-parameter dict.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
params: Raw parameter mapping, possibly containing ``None`` values.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A new dict with all ``None`` entries removed.
|
|
34
|
+
"""
|
|
35
|
+
return {k: v for k, v in params.items() if v is not None}
|
tradezero/http/_retry.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tenacity-based retry configuration shared by both HTTP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from tenacity import (
|
|
9
|
+
RetryCallState,
|
|
10
|
+
retry,
|
|
11
|
+
retry_if_exception,
|
|
12
|
+
stop_after_attempt,
|
|
13
|
+
wait_exponential,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from tradezero.config import DEFAULT_MAX_RETRIES, DEFAULT_RETRY_WAIT_SECONDS
|
|
17
|
+
from tradezero.exceptions import RateLimitError, ServerError
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_retryable(exc: BaseException) -> bool:
|
|
23
|
+
"""Return ``True`` for errors that warrant an automatic retry.
|
|
24
|
+
|
|
25
|
+
Retries are attempted for:
|
|
26
|
+
|
|
27
|
+
- ``httpx.TransportError`` (network-level failures, e.g. connection reset)
|
|
28
|
+
- :class:`~tradezero.exceptions.RateLimitError` (HTTP 429 Too Many Requests)
|
|
29
|
+
- :class:`~tradezero.exceptions.ServerError` (HTTP 5xx server-side errors)
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
exc: The exception raised by the HTTP call.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
``True`` if the request should be retried.
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(exc, httpx.TransportError):
|
|
38
|
+
return True
|
|
39
|
+
if isinstance(exc, (RateLimitError, ServerError)):
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _log_retry(retry_state: RetryCallState) -> None:
|
|
45
|
+
"""Log each retry attempt at WARNING level.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
retry_state: Tenacity state object for the current retry cycle.
|
|
49
|
+
"""
|
|
50
|
+
logger.warning(
|
|
51
|
+
"TradeZero SDK — retry attempt %d/%d after error: %s",
|
|
52
|
+
retry_state.attempt_number,
|
|
53
|
+
DEFAULT_MAX_RETRIES,
|
|
54
|
+
retry_state.outcome.exception() if retry_state.outcome else "unknown",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
#: Pre-built ``@retry`` decorator used by both sync and async clients.
|
|
59
|
+
sdk_retry = retry(
|
|
60
|
+
retry=retry_if_exception(_is_retryable),
|
|
61
|
+
stop=stop_after_attempt(DEFAULT_MAX_RETRIES),
|
|
62
|
+
wait=wait_exponential(
|
|
63
|
+
multiplier=DEFAULT_RETRY_WAIT_SECONDS,
|
|
64
|
+
min=DEFAULT_RETRY_WAIT_SECONDS,
|
|
65
|
+
max=60,
|
|
66
|
+
),
|
|
67
|
+
before_sleep=_log_retry,
|
|
68
|
+
reraise=True,
|
|
69
|
+
)
|