etrade-python 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.
- etrade_python/__init__.py +264 -0
- etrade_python/_dates.py +152 -0
- etrade_python/_version.py +3 -0
- etrade_python/accounts/__init__.py +31 -0
- etrade_python/accounts/models.py +204 -0
- etrade_python/accounts/service.py +136 -0
- etrade_python/alerts/__init__.py +23 -0
- etrade_python/alerts/models.py +180 -0
- etrade_python/alerts/service.py +128 -0
- etrade_python/auth/__init__.py +31 -0
- etrade_python/auth/credentials.py +85 -0
- etrade_python/auth/oauth.py +268 -0
- etrade_python/auth/session.py +154 -0
- etrade_python/auth/stores/__init__.py +7 -0
- etrade_python/auth/stores/base.py +18 -0
- etrade_python/auth/stores/keyring.py +49 -0
- etrade_python/auth/stores/memory.py +19 -0
- etrade_python/cli/__init__.py +1 -0
- etrade_python/cli/app.py +1623 -0
- etrade_python/client.py +84 -0
- etrade_python/config.py +43 -0
- etrade_python/enums.py +19 -0
- etrade_python/exceptions.py +68 -0
- etrade_python/market/__init__.py +59 -0
- etrade_python/market/models.py +811 -0
- etrade_python/market/service.py +168 -0
- etrade_python/orders/__init__.py +107 -0
- etrade_python/orders/builders.py +617 -0
- etrade_python/orders/models.py +1009 -0
- etrade_python/orders/service.py +259 -0
- etrade_python/orders/values.py +67 -0
- etrade_python/portfolio/__init__.py +35 -0
- etrade_python/portfolio/models.py +429 -0
- etrade_python/portfolio/service.py +99 -0
- etrade_python/py.typed +0 -0
- etrade_python/transactions/__init__.py +27 -0
- etrade_python/transactions/models.py +201 -0
- etrade_python/transactions/service.py +138 -0
- etrade_python/transport/__init__.py +7 -0
- etrade_python/transport/auth.py +17 -0
- etrade_python/transport/http.py +240 -0
- etrade_python/transport/logging.py +38 -0
- etrade_python/transport/response.py +243 -0
- etrade_python/transport/retry.py +75 -0
- etrade_python-1.0.0.dist-info/METADATA +513 -0
- etrade_python-1.0.0.dist-info/RECORD +49 -0
- etrade_python-1.0.0.dist-info/WHEEL +4 -0
- etrade_python-1.0.0.dist-info/entry_points.txt +2 -0
- etrade_python-1.0.0.dist-info/licenses/LICENSE +202 -0
etrade_python/client.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The client composition root."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from etrade_python.accounts.service import AccountsService
|
|
6
|
+
from etrade_python.alerts.service import AlertsService
|
|
7
|
+
from etrade_python.auth.oauth import OAuthClient
|
|
8
|
+
from etrade_python.auth.session import SessionAuthenticator, SessionManager
|
|
9
|
+
from etrade_python.auth.stores import CredentialStore, KeyringCredentialStore
|
|
10
|
+
from etrade_python.config import ETradeSettings
|
|
11
|
+
from etrade_python.exceptions import ETradeValidationError
|
|
12
|
+
from etrade_python.market.service import MarketService
|
|
13
|
+
from etrade_python.orders.service import OrdersService
|
|
14
|
+
from etrade_python.portfolio.service import PortfolioService
|
|
15
|
+
from etrade_python.transactions.service import TransactionsService
|
|
16
|
+
from etrade_python.transport.auth import RequestAuthenticator
|
|
17
|
+
from etrade_python.transport.http import ApiTransport
|
|
18
|
+
from etrade_python.transport.retry import RetryPolicy
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ETradeClient:
|
|
22
|
+
"""Async client lifecycle with one HTTP client shared by auth and services."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
settings: ETradeSettings,
|
|
27
|
+
*,
|
|
28
|
+
authenticator: RequestAuthenticator | None = None,
|
|
29
|
+
credential_store: CredentialStore | None = None,
|
|
30
|
+
profile: str = "default",
|
|
31
|
+
http_client: httpx.AsyncClient | None = None,
|
|
32
|
+
http_transport: httpx.AsyncBaseTransport | None = None,
|
|
33
|
+
retry_policy: RetryPolicy | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
if http_client is not None and http_transport is not None:
|
|
36
|
+
raise ETradeValidationError("Supply either http_client or http_transport, not both")
|
|
37
|
+
self._owns_client = http_client is None
|
|
38
|
+
self._http_client = http_client or httpx.AsyncClient(
|
|
39
|
+
transport=http_transport,
|
|
40
|
+
timeout=settings.request_timeout_seconds,
|
|
41
|
+
follow_redirects=False,
|
|
42
|
+
trust_env=False,
|
|
43
|
+
)
|
|
44
|
+
self.oauth = OAuthClient(settings, http_client=self._http_client)
|
|
45
|
+
self.session = SessionManager(
|
|
46
|
+
settings=settings,
|
|
47
|
+
oauth_client=self.oauth,
|
|
48
|
+
credential_store=credential_store or KeyringCredentialStore(),
|
|
49
|
+
profile=profile,
|
|
50
|
+
)
|
|
51
|
+
active_authenticator = authenticator or SessionAuthenticator(self.session, self.oauth)
|
|
52
|
+
self._transport = ApiTransport(
|
|
53
|
+
settings,
|
|
54
|
+
authenticator=active_authenticator,
|
|
55
|
+
http_client=self._http_client,
|
|
56
|
+
retry_policy=retry_policy,
|
|
57
|
+
)
|
|
58
|
+
self.accounts = AccountsService(self._transport)
|
|
59
|
+
self.portfolio = PortfolioService(self._transport)
|
|
60
|
+
self.transactions = TransactionsService(self._transport)
|
|
61
|
+
self.market = MarketService(self._transport)
|
|
62
|
+
self.orders = OrdersService(self._transport)
|
|
63
|
+
self.alerts = AlertsService(self._transport)
|
|
64
|
+
self._closed = False
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_environment(cls) -> "ETradeClient":
|
|
68
|
+
"""Load ETRADE_ variables; does not read .env files or authenticate."""
|
|
69
|
+
return cls(ETradeSettings())
|
|
70
|
+
|
|
71
|
+
async def __aenter__(self) -> "ETradeClient":
|
|
72
|
+
await self._transport.__aenter__()
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
async def __aexit__(self, *_: object) -> None:
|
|
76
|
+
await self.aclose()
|
|
77
|
+
|
|
78
|
+
async def aclose(self) -> None:
|
|
79
|
+
if not self._closed:
|
|
80
|
+
self._closed = True
|
|
81
|
+
await self._transport.aclose()
|
|
82
|
+
await self.oauth.aclose()
|
|
83
|
+
if self._owns_client:
|
|
84
|
+
await self._http_client.aclose()
|
etrade_python/config.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Validated settings; dotenv loading is always explicit."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, SecretStr, ValidationError, field_validator
|
|
6
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
+
|
|
8
|
+
from etrade_python.enums import Environment
|
|
9
|
+
from etrade_python.exceptions import ETradeValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ETradeSettings(BaseSettings):
|
|
13
|
+
model_config = SettingsConfigDict(
|
|
14
|
+
env_prefix="ETRADE_", env_file=None, extra="ignore", frozen=True, hide_input_in_errors=True
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
consumer_key: SecretStr = Field(repr=False)
|
|
18
|
+
consumer_secret: SecretStr = Field(repr=False)
|
|
19
|
+
environment: Environment = Environment.SANDBOX
|
|
20
|
+
request_timeout_seconds: float = Field(default=30.0, gt=0, allow_inf_nan=False)
|
|
21
|
+
inactivity_buffer_seconds: int = Field(default=300, ge=0, lt=7200)
|
|
22
|
+
|
|
23
|
+
def __init__(self, **values: Any) -> None:
|
|
24
|
+
# Do not expose Pydantic error inputs through the normal constructor.
|
|
25
|
+
try:
|
|
26
|
+
super().__init__(**values)
|
|
27
|
+
except ValidationError as exc:
|
|
28
|
+
fields = sorted(
|
|
29
|
+
{
|
|
30
|
+
str(part)
|
|
31
|
+
for error in exc.errors(include_input=False, include_context=False)
|
|
32
|
+
for part in error["loc"]
|
|
33
|
+
if part in type(self).model_fields
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
raise ETradeValidationError("Invalid E*TRADE settings: " + ", ".join(fields)) from None
|
|
37
|
+
|
|
38
|
+
@field_validator("consumer_key", "consumer_secret")
|
|
39
|
+
@classmethod
|
|
40
|
+
def nonempty_secret(cls, value: SecretStr) -> SecretStr:
|
|
41
|
+
if not value.get_secret_value().strip():
|
|
42
|
+
raise ValueError("A nonempty credential is required")
|
|
43
|
+
return value
|
etrade_python/enums.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Configuration enums shared across API domains."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Environment(StrEnum):
|
|
7
|
+
SANDBOX = "sandbox"
|
|
8
|
+
PRODUCTION = "production"
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def api_base_url(self) -> str:
|
|
12
|
+
if self is Environment.PRODUCTION:
|
|
13
|
+
return "https://api.etrade.com"
|
|
14
|
+
return "https://apisb.etrade.com"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# E*TRADE uses the same OAuth server for sandbox and production keys.
|
|
18
|
+
OAUTH_BASE_URL = "https://api.etrade.com"
|
|
19
|
+
AUTHORIZATION_URL = "https://us.etrade.com/e/t/etws/authorize"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Stable public errors. Never attach raw HTTP requests or responses."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ETradeError(Exception):
|
|
5
|
+
"""Base class for library failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ETradeAuthenticationError(ETradeError):
|
|
9
|
+
"""Authentication failed or is unavailable."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthenticationRequired(ETradeAuthenticationError):
|
|
13
|
+
"""Human authorization or an authentication provider is required."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthorizationExpired(ETradeAuthenticationError):
|
|
17
|
+
"""The daily authorization has expired."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ETradeValidationError(ETradeError):
|
|
21
|
+
"""Local settings or request arguments are invalid."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ETradeApiError(ETradeError):
|
|
25
|
+
"""Broker failure with sanitized diagnostic metadata."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
message: str,
|
|
30
|
+
*,
|
|
31
|
+
status_code: int | None = None,
|
|
32
|
+
broker_code: str | None = None,
|
|
33
|
+
broker_message: str | None = None,
|
|
34
|
+
request_id: str | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.status_code = status_code
|
|
38
|
+
self.broker_code = broker_code
|
|
39
|
+
self.broker_message = broker_message
|
|
40
|
+
self.request_id = request_id
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ETradeRateLimitError(ETradeApiError):
|
|
44
|
+
"""The broker rejected a request because of a rate limit."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ETradeNotFoundError(ETradeApiError):
|
|
48
|
+
"""The requested resource was not found."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ETradeUnavailableError(ETradeApiError):
|
|
52
|
+
"""The broker is temporarily unavailable."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ETradeOrderError(ETradeApiError):
|
|
56
|
+
"""An order operation was rejected."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ETradeTransportError(ETradeError):
|
|
60
|
+
"""The HTTP exchange failed; a mutation's outcome may be unknown."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ETradeResponseError(ETradeApiError):
|
|
64
|
+
"""The broker returned an invalid or unexpected response."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ETradeHttpAuthenticationError(ETradeAuthenticationError, ETradeApiError):
|
|
68
|
+
"""An HTTP authentication rejection, including sanitized broker metadata."""
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Market data service and models."""
|
|
2
|
+
|
|
3
|
+
from etrade_python.market.models import (
|
|
4
|
+
ExtendedHourQuoteDetails,
|
|
5
|
+
LookupProduct,
|
|
6
|
+
MutualFundQuoteDetails,
|
|
7
|
+
NetAsset,
|
|
8
|
+
OptionChainPair,
|
|
9
|
+
OptionChainRequest,
|
|
10
|
+
OptionChainResponse,
|
|
11
|
+
OptionContract,
|
|
12
|
+
OptionDeliverable,
|
|
13
|
+
OptionExpiration,
|
|
14
|
+
OptionExpirationsRequest,
|
|
15
|
+
OptionExpirationsResponse,
|
|
16
|
+
OptionGreeks,
|
|
17
|
+
Product,
|
|
18
|
+
ProductLookupRequest,
|
|
19
|
+
ProductLookupResponse,
|
|
20
|
+
Quote,
|
|
21
|
+
QuoteDetails,
|
|
22
|
+
QuoteMessage,
|
|
23
|
+
QuoteMessages,
|
|
24
|
+
QuotesRequest,
|
|
25
|
+
QuotesResponse,
|
|
26
|
+
Redemption,
|
|
27
|
+
RedemptionValues,
|
|
28
|
+
SaleChargeValues,
|
|
29
|
+
)
|
|
30
|
+
from etrade_python.market.service import MarketService
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SaleChargeValues",
|
|
34
|
+
"RedemptionValues",
|
|
35
|
+
"Redemption",
|
|
36
|
+
"QuoteMessages",
|
|
37
|
+
"QuoteMessage",
|
|
38
|
+
"OptionDeliverable",
|
|
39
|
+
"NetAsset",
|
|
40
|
+
"MutualFundQuoteDetails",
|
|
41
|
+
"ExtendedHourQuoteDetails",
|
|
42
|
+
"LookupProduct",
|
|
43
|
+
"MarketService",
|
|
44
|
+
"OptionChainPair",
|
|
45
|
+
"OptionChainRequest",
|
|
46
|
+
"OptionChainResponse",
|
|
47
|
+
"OptionContract",
|
|
48
|
+
"OptionExpiration",
|
|
49
|
+
"OptionExpirationsRequest",
|
|
50
|
+
"OptionExpirationsResponse",
|
|
51
|
+
"OptionGreeks",
|
|
52
|
+
"Product",
|
|
53
|
+
"ProductLookupRequest",
|
|
54
|
+
"ProductLookupResponse",
|
|
55
|
+
"Quote",
|
|
56
|
+
"QuoteDetails",
|
|
57
|
+
"QuotesRequest",
|
|
58
|
+
"QuotesResponse",
|
|
59
|
+
]
|