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.
Files changed (49) hide show
  1. etrade_python/__init__.py +264 -0
  2. etrade_python/_dates.py +152 -0
  3. etrade_python/_version.py +3 -0
  4. etrade_python/accounts/__init__.py +31 -0
  5. etrade_python/accounts/models.py +204 -0
  6. etrade_python/accounts/service.py +136 -0
  7. etrade_python/alerts/__init__.py +23 -0
  8. etrade_python/alerts/models.py +180 -0
  9. etrade_python/alerts/service.py +128 -0
  10. etrade_python/auth/__init__.py +31 -0
  11. etrade_python/auth/credentials.py +85 -0
  12. etrade_python/auth/oauth.py +268 -0
  13. etrade_python/auth/session.py +154 -0
  14. etrade_python/auth/stores/__init__.py +7 -0
  15. etrade_python/auth/stores/base.py +18 -0
  16. etrade_python/auth/stores/keyring.py +49 -0
  17. etrade_python/auth/stores/memory.py +19 -0
  18. etrade_python/cli/__init__.py +1 -0
  19. etrade_python/cli/app.py +1623 -0
  20. etrade_python/client.py +84 -0
  21. etrade_python/config.py +43 -0
  22. etrade_python/enums.py +19 -0
  23. etrade_python/exceptions.py +68 -0
  24. etrade_python/market/__init__.py +59 -0
  25. etrade_python/market/models.py +811 -0
  26. etrade_python/market/service.py +168 -0
  27. etrade_python/orders/__init__.py +107 -0
  28. etrade_python/orders/builders.py +617 -0
  29. etrade_python/orders/models.py +1009 -0
  30. etrade_python/orders/service.py +259 -0
  31. etrade_python/orders/values.py +67 -0
  32. etrade_python/portfolio/__init__.py +35 -0
  33. etrade_python/portfolio/models.py +429 -0
  34. etrade_python/portfolio/service.py +99 -0
  35. etrade_python/py.typed +0 -0
  36. etrade_python/transactions/__init__.py +27 -0
  37. etrade_python/transactions/models.py +201 -0
  38. etrade_python/transactions/service.py +138 -0
  39. etrade_python/transport/__init__.py +7 -0
  40. etrade_python/transport/auth.py +17 -0
  41. etrade_python/transport/http.py +240 -0
  42. etrade_python/transport/logging.py +38 -0
  43. etrade_python/transport/response.py +243 -0
  44. etrade_python/transport/retry.py +75 -0
  45. etrade_python-1.0.0.dist-info/METADATA +513 -0
  46. etrade_python-1.0.0.dist-info/RECORD +49 -0
  47. etrade_python-1.0.0.dist-info/WHEEL +4 -0
  48. etrade_python-1.0.0.dist-info/entry_points.txt +2 -0
  49. etrade_python-1.0.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,168 @@
1
+ """Market API service."""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any, cast
5
+ from urllib.parse import quote
6
+
7
+ from pydantic import ValidationError
8
+
9
+ from etrade_python.exceptions import ETradeResponseError, ETradeValidationError
10
+ from etrade_python.market.models import (
11
+ OptionChainRequest,
12
+ OptionChainResponse,
13
+ OptionExpirationsRequest,
14
+ OptionExpirationsResponse,
15
+ ProductLookupRequest,
16
+ ProductLookupResponse,
17
+ Quote,
18
+ QuotesRequest,
19
+ QuotesResponse,
20
+ )
21
+ from etrade_python.transport.http import ApiTransport
22
+ from etrade_python.transport.response import (
23
+ JsonValue,
24
+ raise_response_validation_error,
25
+ validate_response_shape,
26
+ )
27
+ from etrade_python.transport.retry import RetrySafety
28
+
29
+
30
+ class MarketService:
31
+ """Access E*TRADE market data endpoints."""
32
+
33
+ def __init__(self, transport: ApiTransport) -> None:
34
+ self._transport = transport
35
+
36
+ async def get_quote(self, symbol: str, request: QuotesRequest | None = None) -> Quote:
37
+ response = await self.get_quotes([symbol], request)
38
+ if not response.quotes:
39
+ raise ETradeResponseError("Invalid quote response: no quotes returned")
40
+ requested = symbol.upper()
41
+ for quote_data in response.quotes:
42
+ if quote_data.product is not None and quote_data.product.symbol == requested:
43
+ return quote_data
44
+ return response.quotes[0]
45
+
46
+ async def get_quotes(
47
+ self, symbols: Sequence[str], request: QuotesRequest | None = None
48
+ ) -> QuotesResponse:
49
+ quote_request = request or QuotesRequest()
50
+ normalized_symbols = _normalize_symbols(
51
+ symbols, override_symbol_count=quote_request.override_symbol_count is True
52
+ )
53
+ response = await self._transport.request(
54
+ "GET",
55
+ f"/v1/market/quote/{_quote_path(','.join(normalized_symbols))}.json",
56
+ params=quote_request.query_params(),
57
+ safety=RetrySafety.SAFE_READ,
58
+ operation="market.quotes",
59
+ )
60
+ data = _require_mapping(response.data, "quote")
61
+ quote_data = _unwrap(data, "QuoteResponse", "quote")
62
+ try:
63
+ return validate_response_shape(
64
+ QuotesResponse.model_validate(quote_data), "quote", collection_field="quotes"
65
+ )
66
+ except ValidationError as error:
67
+ raise_response_validation_error("Invalid quote response", error)
68
+
69
+ async def lookup_product(self, search: str) -> ProductLookupResponse:
70
+ lookup_request = ProductLookupRequest(search=search)
71
+ response = await self._transport.request(
72
+ "GET",
73
+ f"/v1/market/lookup/{_quote_path(lookup_request.search)}.json",
74
+ safety=RetrySafety.SAFE_READ,
75
+ operation="market.lookup",
76
+ )
77
+ data = _require_mapping(response.data, "product lookup")
78
+ lookup_data = _unwrap(data, "LookupResponse", "product lookup")
79
+ try:
80
+ return validate_response_shape(
81
+ ProductLookupResponse.model_validate(lookup_data),
82
+ "product lookup",
83
+ collection_field="products",
84
+ )
85
+ except ValidationError as error:
86
+ raise_response_validation_error("Invalid product lookup response", error)
87
+
88
+ async def get_option_expirations(
89
+ self, symbol: str, request: OptionExpirationsRequest | None = None
90
+ ) -> OptionExpirationsResponse:
91
+ normalized_symbol = _normalize_symbol(symbol)
92
+ expirations_request = request or OptionExpirationsRequest()
93
+ response = await self._transport.request(
94
+ "GET",
95
+ "/v1/market/optionexpiredate.json",
96
+ params=expirations_request.query_params(normalized_symbol),
97
+ safety=RetrySafety.SAFE_READ,
98
+ operation="market.option_expirations",
99
+ )
100
+ data = _require_mapping(response.data, "option expirations")
101
+ expirations_data = _unwrap(data, "OptionExpireDateResponse", "option expirations")
102
+ try:
103
+ return validate_response_shape(
104
+ OptionExpirationsResponse.model_validate(expirations_data),
105
+ "option expirations",
106
+ collection_field="expiration_dates",
107
+ )
108
+ except ValidationError as error:
109
+ raise_response_validation_error("Invalid option expirations response", error)
110
+
111
+ async def get_option_chain(
112
+ self, symbol: str, request: OptionChainRequest | None = None
113
+ ) -> OptionChainResponse:
114
+ normalized_symbol = _normalize_symbol(symbol)
115
+ chain_request = request or OptionChainRequest()
116
+ response = await self._transport.request(
117
+ "GET",
118
+ "/v1/market/optionchains.json",
119
+ params=chain_request.query_params(normalized_symbol),
120
+ safety=RetrySafety.SAFE_READ,
121
+ operation="market.option_chain",
122
+ )
123
+ data = _require_mapping(response.data, "option chain")
124
+ chain_data = _unwrap(data, "OptionChainResponse", "option chain")
125
+ try:
126
+ return validate_response_shape(
127
+ OptionChainResponse.model_validate(chain_data),
128
+ "option chain",
129
+ collection_field="option_pairs",
130
+ )
131
+ except ValidationError as error:
132
+ raise_response_validation_error("Invalid option chain response", error)
133
+
134
+
135
+ def _normalize_symbol(symbol: str) -> str:
136
+ normalized = symbol.strip().upper()
137
+ if not normalized:
138
+ raise ETradeValidationError("symbol is required")
139
+ return normalized
140
+
141
+
142
+ def _normalize_symbols(symbols: Sequence[str], *, override_symbol_count: bool) -> list[str]:
143
+ if isinstance(symbols, str):
144
+ raise ETradeValidationError("symbols must be a non-string sequence")
145
+ normalized = [_normalize_symbol(symbol) for symbol in symbols]
146
+ if not normalized:
147
+ raise ETradeValidationError("at least one symbol is required")
148
+ max_symbols = 50 if override_symbol_count else 25
149
+ if len(normalized) > max_symbols:
150
+ raise ETradeValidationError(f"at most {max_symbols} symbols are allowed")
151
+ return normalized
152
+
153
+
154
+ def _quote_path(value: str) -> str:
155
+ return quote(value, safe=",:")
156
+
157
+
158
+ def _require_mapping(data: JsonValue, response_name: str) -> dict[str, Any]:
159
+ if not isinstance(data, dict):
160
+ raise ETradeResponseError(f"Invalid {response_name} response")
161
+ return cast(dict[str, Any], data)
162
+
163
+
164
+ def _unwrap(data: dict[str, Any], envelope: str, response_name: str) -> dict[str, Any]:
165
+ value = data.get(envelope, data)
166
+ if not isinstance(value, dict):
167
+ raise ETradeResponseError(f"Invalid {response_name} response")
168
+ return cast(dict[str, Any], value)
@@ -0,0 +1,107 @@
1
+ """Order service and models."""
2
+
3
+ from etrade_python.orders.builders import (
4
+ VALID_MARKET_SESSIONS,
5
+ VALID_ORDER_ACTIONS,
6
+ VALID_ORDER_TERMS,
7
+ VALID_ORDER_TYPES,
8
+ VALID_PRICE_TYPES,
9
+ VALID_QUANTITY_TYPES,
10
+ VALID_SECURITY_TYPES,
11
+ MarketSession,
12
+ OrderAction,
13
+ OrderBuilder,
14
+ OrderTerm,
15
+ OrderType,
16
+ PriceType,
17
+ QuantityType,
18
+ SecurityType,
19
+ )
20
+ from etrade_python.orders.models import (
21
+ CancelOrderRequest,
22
+ CancelOrderResponse,
23
+ CashBuyingPowerDetails,
24
+ Disclosure,
25
+ DtBuyingPowerDetails,
26
+ Events,
27
+ MarginBuyingPowerDetails,
28
+ Message,
29
+ Messages,
30
+ MFQuantity,
31
+ Order,
32
+ OrderBuyPowerEffect,
33
+ OrderDetail,
34
+ OrderDetailRequest,
35
+ OrderEvent,
36
+ OrderId,
37
+ OrderInstrument,
38
+ OrderInstrumentRequest,
39
+ OrderLot,
40
+ OrderLots,
41
+ OrderProduct,
42
+ OrderProductId,
43
+ OrderResponseId,
44
+ OrderResponseProduct,
45
+ OrdersRequest,
46
+ OrdersResponse,
47
+ PlaceOrderRequest,
48
+ PlaceOrderResponse,
49
+ PortfolioMargin,
50
+ PreviewId,
51
+ PreviewOrderRequest,
52
+ PreviewOrderResponse,
53
+ PreviewResponseId,
54
+ )
55
+ from etrade_python.orders.service import OrdersService
56
+
57
+ __all__ = [
58
+ "PreviewResponseId",
59
+ "OrderResponseId",
60
+ "OrderResponseProduct",
61
+ "PortfolioMargin",
62
+ "OrderProductId",
63
+ "MarginBuyingPowerDetails",
64
+ "DtBuyingPowerDetails",
65
+ "CashBuyingPowerDetails",
66
+ "OrderBuilder",
67
+ "VALID_SECURITY_TYPES",
68
+ "VALID_QUANTITY_TYPES",
69
+ "VALID_PRICE_TYPES",
70
+ "VALID_ORDER_TYPES",
71
+ "VALID_ORDER_TERMS",
72
+ "VALID_ORDER_ACTIONS",
73
+ "VALID_MARKET_SESSIONS",
74
+ "MarketSession",
75
+ "SecurityType",
76
+ "QuantityType",
77
+ "PriceType",
78
+ "OrderType",
79
+ "OrderTerm",
80
+ "OrderAction",
81
+ "CancelOrderRequest",
82
+ "CancelOrderResponse",
83
+ "Disclosure",
84
+ "Events",
85
+ "Message",
86
+ "Messages",
87
+ "MFQuantity",
88
+ "Order",
89
+ "OrderBuyPowerEffect",
90
+ "OrderDetail",
91
+ "OrderDetailRequest",
92
+ "OrderEvent",
93
+ "OrderId",
94
+ "OrderInstrument",
95
+ "OrderInstrumentRequest",
96
+ "OrderLot",
97
+ "OrderLots",
98
+ "OrderProduct",
99
+ "OrdersRequest",
100
+ "OrdersResponse",
101
+ "OrdersService",
102
+ "PlaceOrderRequest",
103
+ "PlaceOrderResponse",
104
+ "PreviewId",
105
+ "PreviewOrderRequest",
106
+ "PreviewOrderResponse",
107
+ ]