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
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
"""Typed market request and response models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
8
|
+
|
|
9
|
+
from etrade_python._dates import parse_broker_datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BrokerModel(BaseModel):
|
|
13
|
+
"""Base response model that discards unmodeled broker fields."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(extra="ignore", frozen=True, populate_by_name=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _bool_query(value: bool | None) -> str | None:
|
|
19
|
+
if value is None:
|
|
20
|
+
return None
|
|
21
|
+
return "true" if value else "false"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class QuotesRequest(BaseModel):
|
|
25
|
+
"""Query parameters for quote lookup."""
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(frozen=True)
|
|
28
|
+
|
|
29
|
+
detail_flag: str | None = None
|
|
30
|
+
require_earnings_date: bool | None = None
|
|
31
|
+
override_symbol_count: bool | None = None
|
|
32
|
+
skip_mini_options_check: bool | None = None
|
|
33
|
+
|
|
34
|
+
@field_validator("detail_flag")
|
|
35
|
+
@classmethod
|
|
36
|
+
def valid_detail_flag(cls, value: str | None) -> str | None:
|
|
37
|
+
if value is None:
|
|
38
|
+
return None
|
|
39
|
+
normalized = value.strip().upper()
|
|
40
|
+
if not normalized:
|
|
41
|
+
raise ValueError("A nonempty value is required")
|
|
42
|
+
if normalized not in {"ALL", "FUNDAMENTAL", "INTRADAY", "OPTIONS", "WEEK_52", "MF_DETAIL"}:
|
|
43
|
+
raise ValueError("Unsupported quote detail flag")
|
|
44
|
+
return normalized
|
|
45
|
+
|
|
46
|
+
def query_params(self) -> dict[str, str | None]:
|
|
47
|
+
return {
|
|
48
|
+
"detailFlag": self.detail_flag,
|
|
49
|
+
"requireEarningsDate": _bool_query(self.require_earnings_date),
|
|
50
|
+
"overrideSymbolCount": _bool_query(self.override_symbol_count),
|
|
51
|
+
"skipMiniOptionsCheck": _bool_query(self.skip_mini_options_check),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ProductLookupRequest(BaseModel):
|
|
56
|
+
"""Path parameter for product lookup."""
|
|
57
|
+
|
|
58
|
+
model_config = ConfigDict(frozen=True)
|
|
59
|
+
|
|
60
|
+
search: str
|
|
61
|
+
|
|
62
|
+
@field_validator("search")
|
|
63
|
+
@classmethod
|
|
64
|
+
def nonempty_search(cls, value: str) -> str:
|
|
65
|
+
if not value.strip():
|
|
66
|
+
raise ValueError("A nonempty value is required")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OptionExpirationsRequest(BaseModel):
|
|
71
|
+
"""Query parameters for option expiration dates."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(frozen=True)
|
|
74
|
+
|
|
75
|
+
expiry_type: str | None = None
|
|
76
|
+
|
|
77
|
+
@field_validator("expiry_type")
|
|
78
|
+
@classmethod
|
|
79
|
+
def valid_expiry_type(cls, value: str | None) -> str | None:
|
|
80
|
+
if value is None:
|
|
81
|
+
return None
|
|
82
|
+
normalized = value.strip().upper()
|
|
83
|
+
if not normalized:
|
|
84
|
+
raise ValueError("A nonempty value is required")
|
|
85
|
+
if normalized not in {
|
|
86
|
+
"UNSPECIFIED",
|
|
87
|
+
"DAILY",
|
|
88
|
+
"WEEKLY",
|
|
89
|
+
"MONTHLY",
|
|
90
|
+
"QUARTERLY",
|
|
91
|
+
"VIX",
|
|
92
|
+
"ALL",
|
|
93
|
+
"MONTHEND",
|
|
94
|
+
}:
|
|
95
|
+
raise ValueError("Unsupported option expiration type")
|
|
96
|
+
return normalized
|
|
97
|
+
|
|
98
|
+
def query_params(self, symbol: str) -> dict[str, str | None]:
|
|
99
|
+
return {"symbol": symbol, "expiryType": self.expiry_type}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OptionChainRequest(BaseModel):
|
|
103
|
+
"""Query parameters for option chains."""
|
|
104
|
+
|
|
105
|
+
model_config = ConfigDict(frozen=True)
|
|
106
|
+
|
|
107
|
+
expiry_year: int | None = Field(default=None, ge=1900)
|
|
108
|
+
expiry_month: int | None = Field(default=None, ge=1, le=12)
|
|
109
|
+
expiry_day: int | None = Field(default=None, ge=1, le=31)
|
|
110
|
+
strike_price_near: Decimal | None = Field(default=None, ge=0)
|
|
111
|
+
no_of_strikes: int | None = Field(default=None, ge=1)
|
|
112
|
+
include_weekly: bool | None = None
|
|
113
|
+
skip_adjusted: bool | None = None
|
|
114
|
+
option_category: str | None = None
|
|
115
|
+
chain_type: str | None = None
|
|
116
|
+
price_type: str | None = None
|
|
117
|
+
|
|
118
|
+
@field_validator("option_category")
|
|
119
|
+
@classmethod
|
|
120
|
+
def valid_option_category(cls, value: str | None) -> str | None:
|
|
121
|
+
return _normalize_choice(value, {"STANDARD", "ALL", "MINI"}, "option category")
|
|
122
|
+
|
|
123
|
+
@field_validator("chain_type")
|
|
124
|
+
@classmethod
|
|
125
|
+
def valid_chain_type(cls, value: str | None) -> str | None:
|
|
126
|
+
return _normalize_choice(value, {"CALL", "PUT", "CALLPUT"}, "chain type")
|
|
127
|
+
|
|
128
|
+
@field_validator("price_type")
|
|
129
|
+
@classmethod
|
|
130
|
+
def valid_price_type(cls, value: str | None) -> str | None:
|
|
131
|
+
return _normalize_choice(value, {"ATNM", "ALL"}, "price type")
|
|
132
|
+
|
|
133
|
+
def query_params(self, symbol: str) -> dict[str, str | int | None]:
|
|
134
|
+
return {
|
|
135
|
+
"symbol": symbol,
|
|
136
|
+
"expiryYear": self.expiry_year,
|
|
137
|
+
"expiryMonth": self.expiry_month,
|
|
138
|
+
"expiryDay": self.expiry_day,
|
|
139
|
+
"strikePriceNear": str(self.strike_price_near)
|
|
140
|
+
if self.strike_price_near is not None
|
|
141
|
+
else None,
|
|
142
|
+
"noOfStrikes": self.no_of_strikes,
|
|
143
|
+
"includeWeekly": _bool_query(self.include_weekly),
|
|
144
|
+
"skipAdjusted": _bool_query(self.skip_adjusted),
|
|
145
|
+
"optionCategory": self.option_category,
|
|
146
|
+
"chainType": self.chain_type,
|
|
147
|
+
"priceType": self.price_type,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _normalize_choice(value: str | None, choices: set[str], label: str) -> str | None:
|
|
152
|
+
if value is None:
|
|
153
|
+
return None
|
|
154
|
+
normalized = value.strip().upper()
|
|
155
|
+
if not normalized:
|
|
156
|
+
raise ValueError("A nonempty value is required")
|
|
157
|
+
if normalized not in choices:
|
|
158
|
+
raise ValueError(f"Unsupported {label}")
|
|
159
|
+
return normalized
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ProductId(BrokerModel):
|
|
163
|
+
symbol: str | None = None
|
|
164
|
+
type_code: str | None = Field(default=None, alias="typeCode")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Product(BrokerModel):
|
|
168
|
+
symbol: str | None = None
|
|
169
|
+
security_type: str | None = Field(default=None, alias="securityType")
|
|
170
|
+
security_sub_type: str | None = Field(default=None, alias="securitySubType")
|
|
171
|
+
call_put: str | None = Field(default=None, alias="callPut")
|
|
172
|
+
expiry_year: int | None = Field(default=None, alias="expiryYear")
|
|
173
|
+
expiry_month: int | None = Field(default=None, alias="expiryMonth")
|
|
174
|
+
expiry_day: int | None = Field(default=None, alias="expiryDay")
|
|
175
|
+
strike_price: Decimal | None = Field(default=None, alias="strikePrice")
|
|
176
|
+
expiry_type: str | None = Field(default=None, alias="expiryType")
|
|
177
|
+
product_id: ProductId | None = Field(default=None, alias="productId")
|
|
178
|
+
|
|
179
|
+
@model_validator(mode="before")
|
|
180
|
+
@classmethod
|
|
181
|
+
def normalize_nested_aliases(cls, value: object) -> object:
|
|
182
|
+
if not isinstance(value, dict):
|
|
183
|
+
return value
|
|
184
|
+
data = dict(cast(dict[str, Any], value))
|
|
185
|
+
if "ProductId" in data and "productId" not in data:
|
|
186
|
+
data["productId"] = data["ProductId"]
|
|
187
|
+
data.pop("ProductId", None)
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class LookupProduct(BrokerModel):
|
|
192
|
+
symbol: str | None = None
|
|
193
|
+
description: str | None = None
|
|
194
|
+
type: str | None = None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _empty_lookup_products() -> list[LookupProduct]:
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ProductLookupResponse(BrokerModel):
|
|
202
|
+
products: list[LookupProduct] = Field(default_factory=_empty_lookup_products, alias="data")
|
|
203
|
+
|
|
204
|
+
@model_validator(mode="before")
|
|
205
|
+
@classmethod
|
|
206
|
+
def normalize_response(cls, value: object) -> object:
|
|
207
|
+
if not isinstance(value, dict):
|
|
208
|
+
return value
|
|
209
|
+
data = dict(cast(dict[str, Any], value))
|
|
210
|
+
if "Data" in data and "data" not in data:
|
|
211
|
+
data["data"] = data["Data"]
|
|
212
|
+
data.pop("Data", None)
|
|
213
|
+
return data
|
|
214
|
+
|
|
215
|
+
@field_validator("products", mode="before")
|
|
216
|
+
@classmethod
|
|
217
|
+
def normalize_products(cls, value: object) -> object:
|
|
218
|
+
if value is None:
|
|
219
|
+
return []
|
|
220
|
+
if isinstance(value, dict):
|
|
221
|
+
return [cast(dict[str, Any], value)]
|
|
222
|
+
return value
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _normalize_aliases(value: object, aliases: dict[str, str]) -> object:
|
|
226
|
+
if not isinstance(value, dict):
|
|
227
|
+
return value
|
|
228
|
+
data = dict(cast(dict[str, Any], value))
|
|
229
|
+
for source, target in aliases.items():
|
|
230
|
+
if source in data and target not in data:
|
|
231
|
+
data[target] = data[source]
|
|
232
|
+
data.pop(source, None)
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _normalize_collection(value: object, wrappers: tuple[str, ...] = ()) -> object:
|
|
237
|
+
if isinstance(value, dict):
|
|
238
|
+
data = cast(dict[str, Any], value)
|
|
239
|
+
for key in wrappers:
|
|
240
|
+
if key in data:
|
|
241
|
+
value = data[key]
|
|
242
|
+
break
|
|
243
|
+
if value is None or value == {}:
|
|
244
|
+
return []
|
|
245
|
+
if isinstance(value, dict):
|
|
246
|
+
return [cast(dict[str, Any], value)]
|
|
247
|
+
return value
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class OptionGreeks(BrokerModel):
|
|
251
|
+
rho: Decimal | None = None
|
|
252
|
+
vega: Decimal | None = None
|
|
253
|
+
theta: Decimal | None = None
|
|
254
|
+
delta: Decimal | None = None
|
|
255
|
+
gamma: Decimal | None = None
|
|
256
|
+
iv: Decimal | None = None
|
|
257
|
+
current_value: bool | str | None = Field(default=None, alias="currentValue")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ExtendedHourQuoteDetails(BrokerModel):
|
|
261
|
+
last_price: Decimal | None = Field(default=None, alias="lastPrice")
|
|
262
|
+
change: Decimal | None = None
|
|
263
|
+
percent_change: Decimal | None = Field(default=None, alias="percentChange")
|
|
264
|
+
bid: Decimal | None = None
|
|
265
|
+
bid_size: int | None = Field(default=None, alias="bidSize")
|
|
266
|
+
ask: Decimal | None = None
|
|
267
|
+
ask_size: int | None = Field(default=None, alias="askSize")
|
|
268
|
+
volume: int | None = None
|
|
269
|
+
time_of_last_trade: datetime | None = Field(default=None, alias="timeOfLastTrade")
|
|
270
|
+
time_zone: str | None = Field(default=None, alias="timeZone")
|
|
271
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
272
|
+
|
|
273
|
+
@field_validator("time_of_last_trade", mode="before")
|
|
274
|
+
@classmethod
|
|
275
|
+
def parse_time_of_last_trade(cls, value: object) -> datetime | None:
|
|
276
|
+
return parse_broker_datetime(value)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class OptionDeliverable(BrokerModel):
|
|
280
|
+
root_symbol: str | None = Field(default=None, alias="rootSymbol")
|
|
281
|
+
deliverable_symbol: str | None = Field(default=None, alias="deliverableSymbol")
|
|
282
|
+
deliverable_type_code: str | None = Field(default=None, alias="deliverableTypeCode")
|
|
283
|
+
deliverable_exchange_code: str | None = Field(default=None, alias="deliverableExchangeCode")
|
|
284
|
+
deliverable_strike_percent: Decimal | None = Field(
|
|
285
|
+
default=None, alias="deliverableStrikePercent"
|
|
286
|
+
)
|
|
287
|
+
deliverable_cil_shares: Decimal | None = Field(default=None, alias="deliverableCILShares")
|
|
288
|
+
deliverable_whole_shares: int | None = Field(default=None, alias="deliverableWholeShares")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class QuoteMessage(BrokerModel):
|
|
292
|
+
description: str | None = None
|
|
293
|
+
code: int | None = None
|
|
294
|
+
type: str | None = None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _empty_quote_messages() -> list[QuoteMessage]:
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class QuoteMessages(BrokerModel):
|
|
302
|
+
messages: list[QuoteMessage] = Field(default_factory=_empty_quote_messages, alias="message")
|
|
303
|
+
|
|
304
|
+
@model_validator(mode="before")
|
|
305
|
+
@classmethod
|
|
306
|
+
def normalize_response(cls, value: object) -> object:
|
|
307
|
+
return _normalize_aliases(value, {"Message": "message", "messages": "message"})
|
|
308
|
+
|
|
309
|
+
@field_validator("messages", mode="before")
|
|
310
|
+
@classmethod
|
|
311
|
+
def normalize_messages(cls, value: object) -> object:
|
|
312
|
+
return _normalize_collection(value)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class NetAsset(BrokerModel):
|
|
316
|
+
value: Decimal | None = None
|
|
317
|
+
as_of_date: datetime | None = Field(default=None, alias="asOfDate")
|
|
318
|
+
|
|
319
|
+
@field_validator("as_of_date", mode="before")
|
|
320
|
+
@classmethod
|
|
321
|
+
def parse_as_of_date(cls, value: object) -> datetime | None:
|
|
322
|
+
return parse_broker_datetime(value)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class RedemptionValues(BrokerModel):
|
|
326
|
+
low: str | None = None
|
|
327
|
+
high: str | None = None
|
|
328
|
+
percent: str | None = None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class SaleChargeValues(BrokerModel):
|
|
332
|
+
lowhigh: str | None = None
|
|
333
|
+
percent: str | None = None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _empty_front_end_values() -> list[RedemptionValues]:
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _empty_sales_values() -> list[RedemptionValues]:
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class Redemption(BrokerModel):
|
|
345
|
+
min_month: str | None = Field(default=None, alias="minMonth")
|
|
346
|
+
fee_percent: str | None = Field(default=None, alias="feePercent")
|
|
347
|
+
is_front_end: str | None = Field(default=None, alias="isFrontEnd")
|
|
348
|
+
front_end_values: list[RedemptionValues] = Field(
|
|
349
|
+
default_factory=_empty_front_end_values, alias="frontEndValues"
|
|
350
|
+
)
|
|
351
|
+
redemption_duration_type: str | None = Field(default=None, alias="redemptionDurationType")
|
|
352
|
+
is_sales: str | None = Field(default=None, alias="isSales")
|
|
353
|
+
sales_duration_type: str | None = Field(default=None, alias="salesDurationType")
|
|
354
|
+
sales_values: list[RedemptionValues] = Field(
|
|
355
|
+
default_factory=_empty_sales_values, alias="salesValues"
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
@model_validator(mode="before")
|
|
359
|
+
@classmethod
|
|
360
|
+
def normalize_response(cls, value: object) -> object:
|
|
361
|
+
return _normalize_aliases(
|
|
362
|
+
value, {"FrontEndValues": "frontEndValues", "SalesValues": "salesValues"}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
@field_validator("front_end_values", "sales_values", mode="before")
|
|
366
|
+
@classmethod
|
|
367
|
+
def normalize_values(cls, value: object) -> object:
|
|
368
|
+
return _normalize_collection(value, ("Values", "values"))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _empty_option_deliverables() -> list[OptionDeliverable]:
|
|
372
|
+
return []
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class QuoteDetails(BrokerModel):
|
|
376
|
+
adjusted_flag: bool | str | None = Field(default=None, alias="adjustedFlag")
|
|
377
|
+
ask: Decimal | None = None
|
|
378
|
+
ask_size: int | None = Field(default=None, alias="askSize")
|
|
379
|
+
ask_time: datetime | None = Field(default=None, alias="askTime")
|
|
380
|
+
bid: Decimal | None = None
|
|
381
|
+
bid_exchange: str | None = Field(default=None, alias="bidExchange")
|
|
382
|
+
bid_size: int | None = Field(default=None, alias="bidSize")
|
|
383
|
+
bid_time: datetime | None = Field(default=None, alias="bidTime")
|
|
384
|
+
change_close: Decimal | None = Field(default=None, alias="changeClose")
|
|
385
|
+
change_close_percentage: Decimal | None = Field(default=None, alias="changeClosePercentage")
|
|
386
|
+
company_name: str | None = Field(default=None, alias="companyName")
|
|
387
|
+
days_to_expiration: int | None = Field(default=None, alias="daysToExpiration")
|
|
388
|
+
dividend: Decimal | None = None
|
|
389
|
+
eps: Decimal | None = None
|
|
390
|
+
est_earnings: Decimal | None = Field(default=None, alias="estEarnings")
|
|
391
|
+
ex_dividend_date: datetime | None = Field(default=None, alias="exDividendDate")
|
|
392
|
+
high: Decimal | None = None
|
|
393
|
+
high52: Decimal | None = None
|
|
394
|
+
last_trade: Decimal | None = Field(default=None, alias="lastTrade")
|
|
395
|
+
low: Decimal | None = None
|
|
396
|
+
low52: Decimal | None = None
|
|
397
|
+
open: Decimal | None = None
|
|
398
|
+
open_interest: int | None = Field(default=None, alias="openInterest")
|
|
399
|
+
option_style: str | None = Field(default=None, alias="optionStyle")
|
|
400
|
+
option_underlier: str | None = Field(default=None, alias="optionUnderlier")
|
|
401
|
+
previous_close: Decimal | None = Field(default=None, alias="previousClose")
|
|
402
|
+
previous_day_volume: int | None = Field(default=None, alias="previousDayVolume")
|
|
403
|
+
primary_exchange: str | None = Field(default=None, alias="primaryExchange")
|
|
404
|
+
symbol_description: str | None = Field(default=None, alias="symbolDescription")
|
|
405
|
+
total_volume: int | None = Field(default=None, alias="totalVolume")
|
|
406
|
+
market_cap: Decimal | None = Field(default=None, alias="marketCap")
|
|
407
|
+
shares_outstanding: Decimal | None = Field(default=None, alias="sharesOutstanding")
|
|
408
|
+
next_earning_date: datetime | None = Field(default=None, alias="nextEarningDate")
|
|
409
|
+
beta: Decimal | None = None
|
|
410
|
+
yield_: Decimal | None = Field(default=None, alias="yield")
|
|
411
|
+
declared_dividend: Decimal | None = Field(default=None, alias="declaredDividend")
|
|
412
|
+
dividend_payable_date: datetime | None = Field(default=None, alias="dividendPayableDate")
|
|
413
|
+
pe: Decimal | None = None
|
|
414
|
+
week52_low_date: datetime | None = Field(default=None, alias="week52LowDate")
|
|
415
|
+
week52_hi_date: datetime | None = Field(default=None, alias="week52HiDate")
|
|
416
|
+
intrinsic_value: Decimal | None = Field(default=None, alias="intrinsicValue")
|
|
417
|
+
time_premium: Decimal | None = Field(default=None, alias="timePremium")
|
|
418
|
+
option_multiplier: Decimal | None = Field(default=None, alias="optionMultiplier")
|
|
419
|
+
contract_size: Decimal | None = Field(default=None, alias="contractSize")
|
|
420
|
+
expiration_date: datetime | None = Field(default=None, alias="expirationDate")
|
|
421
|
+
time_of_last_trade: datetime | None = Field(default=None, alias="timeOfLastTrade")
|
|
422
|
+
average_volume: int | None = Field(default=None, alias="averageVolume")
|
|
423
|
+
dir_last: str | None = Field(default=None, alias="dirLast")
|
|
424
|
+
option_underlier_exchange: str | None = Field(default=None, alias="optionUnderlierExchange")
|
|
425
|
+
upc: int | None = None
|
|
426
|
+
cash_deliverable: Decimal | None = Field(default=None, alias="cashDeliverable")
|
|
427
|
+
option_previous_bid_price: Decimal | None = Field(default=None, alias="optionPreviousBidPrice")
|
|
428
|
+
option_previous_ask_price: Decimal | None = Field(default=None, alias="optionPreviousAskPrice")
|
|
429
|
+
osi_key: str | None = Field(default=None, alias="osiKey")
|
|
430
|
+
eh_quote: ExtendedHourQuoteDetails | None = Field(default=None, alias="ehQuote")
|
|
431
|
+
option_greeks: OptionGreeks | None = Field(default=None, alias="optionGreeks")
|
|
432
|
+
option_deliverables: list[OptionDeliverable] = Field(
|
|
433
|
+
default_factory=_empty_option_deliverables, alias="optionDeliverableList"
|
|
434
|
+
)
|
|
435
|
+
perf12_months: Decimal | None = Field(default=None, alias="perf12Months")
|
|
436
|
+
|
|
437
|
+
@field_validator(
|
|
438
|
+
"ask_time",
|
|
439
|
+
"bid_time",
|
|
440
|
+
"ex_dividend_date",
|
|
441
|
+
"next_earning_date",
|
|
442
|
+
"dividend_payable_date",
|
|
443
|
+
"week52_low_date",
|
|
444
|
+
"week52_hi_date",
|
|
445
|
+
"expiration_date",
|
|
446
|
+
"time_of_last_trade",
|
|
447
|
+
mode="before",
|
|
448
|
+
)
|
|
449
|
+
@classmethod
|
|
450
|
+
def parse_detail_datetimes(cls, value: object) -> datetime | None:
|
|
451
|
+
return parse_broker_datetime(value)
|
|
452
|
+
|
|
453
|
+
@model_validator(mode="before")
|
|
454
|
+
@classmethod
|
|
455
|
+
def normalize_detail_aliases(cls, value: object) -> object:
|
|
456
|
+
return _normalize_aliases(
|
|
457
|
+
value,
|
|
458
|
+
{
|
|
459
|
+
"EhQuote": "ehQuote",
|
|
460
|
+
"OptionGreeks": "optionGreeks",
|
|
461
|
+
"OptionDeliverableList": "optionDeliverableList",
|
|
462
|
+
},
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
@field_validator("option_deliverables", mode="before")
|
|
466
|
+
@classmethod
|
|
467
|
+
def normalize_deliverables(cls, value: object) -> object:
|
|
468
|
+
return _normalize_collection(value, ("optionDeliverable", "OptionDeliverable"))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _empty_deferred_sales_charges() -> list[SaleChargeValues]:
|
|
472
|
+
return []
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _empty_front_end_sales_charges() -> list[SaleChargeValues]:
|
|
476
|
+
return []
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class MutualFundQuoteDetails(QuoteDetails):
|
|
480
|
+
cusip: str | None = None
|
|
481
|
+
transaction_fee: str | None = Field(default=None, alias="transactionFee")
|
|
482
|
+
early_redemption_fee: str | None = Field(default=None, alias="earlyRedemptionFee")
|
|
483
|
+
availability: str | None = None
|
|
484
|
+
initial_investment: Decimal | None = Field(default=None, alias="initialInvestment")
|
|
485
|
+
subsequent_investment: Decimal | None = Field(default=None, alias="subsequentInvestment")
|
|
486
|
+
fund_family: str | None = Field(default=None, alias="fundFamily")
|
|
487
|
+
fund_name: str | None = Field(default=None, alias="fundName")
|
|
488
|
+
net_asset_value: Decimal | None = Field(default=None, alias="netAssetValue")
|
|
489
|
+
public_offer_price: Decimal | None = Field(default=None, alias="publicOfferPrice")
|
|
490
|
+
net_expense_ratio: Decimal | None = Field(default=None, alias="netExpenseRatio")
|
|
491
|
+
gross_expense_ratio: Decimal | None = Field(default=None, alias="grossExpenseRatio")
|
|
492
|
+
order_cutoff_time: datetime | None = Field(default=None, alias="orderCutoffTime")
|
|
493
|
+
sales_charge: str | None = Field(default=None, alias="salesCharge")
|
|
494
|
+
initial_ira_investment: Decimal | None = Field(default=None, alias="initialIraInvestment")
|
|
495
|
+
subsequent_ira_investment: Decimal | None = Field(default=None, alias="subsequentIraInvestment")
|
|
496
|
+
net_assets: NetAsset | None = Field(default=None, alias="netAssets")
|
|
497
|
+
fund_inception_date: datetime | None = Field(default=None, alias="fundInceptionDate")
|
|
498
|
+
average_annual_returns: Decimal | None = Field(default=None, alias="averageAnnualReturns")
|
|
499
|
+
seven_day_current_yield: Decimal | None = Field(default=None, alias="sevenDayCurrentYield")
|
|
500
|
+
annual_total_return: Decimal | None = Field(default=None, alias="annualTotalReturn")
|
|
501
|
+
weighted_average_maturity: Decimal | None = Field(default=None, alias="weightedAverageMaturity")
|
|
502
|
+
average_annual_return1_yr: Decimal | None = Field(default=None, alias="averageAnnualReturn1Yr")
|
|
503
|
+
average_annual_return3_yr: Decimal | None = Field(default=None, alias="averageAnnualReturn3Yr")
|
|
504
|
+
average_annual_return5_yr: Decimal | None = Field(default=None, alias="averageAnnualReturn5Yr")
|
|
505
|
+
average_annual_return10_yr: Decimal | None = Field(
|
|
506
|
+
default=None, alias="averageAnnualReturn10Yr"
|
|
507
|
+
)
|
|
508
|
+
exchange_name: str | None = Field(default=None, alias="exchangeName")
|
|
509
|
+
since_inception: Decimal | None = Field(default=None, alias="sinceInception")
|
|
510
|
+
quarterly_since_inception: Decimal | None = Field(default=None, alias="quarterlySinceInception")
|
|
511
|
+
actual12_b1_fee: Decimal | None = Field(default=None, alias="actual12B1Fee")
|
|
512
|
+
performance_as_of_date: datetime | None = Field(default=None, alias="performanceAsOfDate")
|
|
513
|
+
qtrly_performance_as_of_date: datetime | None = Field(
|
|
514
|
+
default=None, alias="qtrlyPerformanceAsOfDate"
|
|
515
|
+
)
|
|
516
|
+
redemption: Redemption | None = None
|
|
517
|
+
morning_star_category: str | None = Field(default=None, alias="morningStarCategory")
|
|
518
|
+
monthly_trailing_return1_y: Decimal | None = Field(
|
|
519
|
+
default=None, alias="monthlyTrailingReturn1Y"
|
|
520
|
+
)
|
|
521
|
+
monthly_trailing_return3_y: Decimal | None = Field(
|
|
522
|
+
default=None, alias="monthlyTrailingReturn3Y"
|
|
523
|
+
)
|
|
524
|
+
monthly_trailing_return5_y: Decimal | None = Field(
|
|
525
|
+
default=None, alias="monthlyTrailingReturn5Y"
|
|
526
|
+
)
|
|
527
|
+
monthly_trailing_return10_y: Decimal | None = Field(
|
|
528
|
+
default=None, alias="monthlyTrailingReturn10Y"
|
|
529
|
+
)
|
|
530
|
+
etrade_early_redemption_fee: str | None = Field(default=None, alias="etradeEarlyRedemptionFee")
|
|
531
|
+
max_sales_load: Decimal | None = Field(default=None, alias="maxSalesLoad")
|
|
532
|
+
monthly_trailing_return_ytd: Decimal | None = Field(
|
|
533
|
+
default=None, alias="monthlyTrailingReturnYTD"
|
|
534
|
+
)
|
|
535
|
+
monthly_trailing_return1_m: Decimal | None = Field(
|
|
536
|
+
default=None, alias="monthlyTrailingReturn1M"
|
|
537
|
+
)
|
|
538
|
+
monthly_trailing_return3_m: Decimal | None = Field(
|
|
539
|
+
default=None, alias="monthlyTrailingReturn3M"
|
|
540
|
+
)
|
|
541
|
+
monthly_trailing_return6_m: Decimal | None = Field(
|
|
542
|
+
default=None, alias="monthlyTrailingReturn6M"
|
|
543
|
+
)
|
|
544
|
+
qtrly_trailing_return_ytd: Decimal | None = Field(default=None, alias="qtrlyTrailingReturnYTD")
|
|
545
|
+
qtrly_trailing_return1_m: Decimal | None = Field(default=None, alias="qtrlyTrailingReturn1M")
|
|
546
|
+
qtrly_trailing_return3_m: Decimal | None = Field(default=None, alias="qtrlyTrailingReturn3M")
|
|
547
|
+
qtrly_trailing_return6_m: Decimal | None = Field(default=None, alias="qtrlyTrailingReturn6M")
|
|
548
|
+
deferred_sales_charges: list[SaleChargeValues] = Field(
|
|
549
|
+
default_factory=_empty_deferred_sales_charges, alias="deferredSalesCharges"
|
|
550
|
+
)
|
|
551
|
+
front_end_sales_charges: list[SaleChargeValues] = Field(
|
|
552
|
+
default_factory=_empty_front_end_sales_charges, alias="frontEndSalesCharges"
|
|
553
|
+
)
|
|
554
|
+
exchange_code: str | None = Field(default=None, alias="exchangeCode")
|
|
555
|
+
|
|
556
|
+
@field_validator(
|
|
557
|
+
"order_cutoff_time",
|
|
558
|
+
"fund_inception_date",
|
|
559
|
+
"performance_as_of_date",
|
|
560
|
+
"qtrly_performance_as_of_date",
|
|
561
|
+
mode="before",
|
|
562
|
+
)
|
|
563
|
+
@classmethod
|
|
564
|
+
def parse_fund_datetimes(cls, value: object) -> datetime | None:
|
|
565
|
+
return parse_broker_datetime(value)
|
|
566
|
+
|
|
567
|
+
@model_validator(mode="before")
|
|
568
|
+
@classmethod
|
|
569
|
+
def normalize_fund_aliases(cls, value: object) -> object:
|
|
570
|
+
return _normalize_aliases(
|
|
571
|
+
value,
|
|
572
|
+
{
|
|
573
|
+
"NetAssets": "netAssets",
|
|
574
|
+
"Redemption": "redemption",
|
|
575
|
+
"DeferredSalesCharges": "deferredSalesCharges",
|
|
576
|
+
"FrontEndSalesCharges": "frontEndSalesCharges",
|
|
577
|
+
},
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
@field_validator("deferred_sales_charges", "front_end_sales_charges", mode="before")
|
|
581
|
+
@classmethod
|
|
582
|
+
def normalize_sales_charges(cls, value: object) -> object:
|
|
583
|
+
return _normalize_collection(value, ("saleChargeValues", "SaleChargeValues"))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class Quote(BrokerModel):
|
|
587
|
+
date_time: datetime | None = Field(default=None, alias="dateTime")
|
|
588
|
+
date_time_utc: datetime | None = Field(default=None, alias="dateTimeUTC")
|
|
589
|
+
quote_status: str | None = Field(default=None, alias="quoteStatus")
|
|
590
|
+
ah_flag: bool | str | None = Field(default=None, alias="ahFlag")
|
|
591
|
+
error_message: str | None = Field(default=None, alias="errorMessage")
|
|
592
|
+
time_zone: str | None = Field(default=None, alias="timeZone")
|
|
593
|
+
dst_flag: bool | str | None = Field(default=None, alias="dstFlag")
|
|
594
|
+
has_mini_options: bool | str | None = Field(default=None, alias="hasMiniOptions")
|
|
595
|
+
all: QuoteDetails | None = None
|
|
596
|
+
fundamental: QuoteDetails | None = None
|
|
597
|
+
intraday: QuoteDetails | None = None
|
|
598
|
+
option: QuoteDetails | None = None
|
|
599
|
+
week52: QuoteDetails | None = Field(default=None, alias="week52")
|
|
600
|
+
mutual_fund: MutualFundQuoteDetails | None = Field(default=None, alias="mutualFund")
|
|
601
|
+
product: Product | None = None
|
|
602
|
+
|
|
603
|
+
@field_validator("date_time", "date_time_utc", mode="before")
|
|
604
|
+
@classmethod
|
|
605
|
+
def parse_quote_datetimes(cls, value: object) -> datetime | None:
|
|
606
|
+
return parse_broker_datetime(value)
|
|
607
|
+
|
|
608
|
+
@model_validator(mode="before")
|
|
609
|
+
@classmethod
|
|
610
|
+
def normalize_nested_aliases(cls, value: object) -> object:
|
|
611
|
+
if not isinstance(value, dict):
|
|
612
|
+
return value
|
|
613
|
+
data = dict(cast(dict[str, Any], value))
|
|
614
|
+
aliases = {
|
|
615
|
+
"All": "all",
|
|
616
|
+
"Fundamental": "fundamental",
|
|
617
|
+
"Intraday": "intraday",
|
|
618
|
+
"Option": "option",
|
|
619
|
+
"Week52": "week52",
|
|
620
|
+
"MutualFund": "mutualFund",
|
|
621
|
+
"Product": "product",
|
|
622
|
+
}
|
|
623
|
+
for source, target in aliases.items():
|
|
624
|
+
if source in data and target not in data:
|
|
625
|
+
data[target] = data[source]
|
|
626
|
+
data.pop(source, None)
|
|
627
|
+
return data
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _empty_quotes() -> list[Quote]:
|
|
631
|
+
return []
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class QuotesResponse(BrokerModel):
|
|
635
|
+
messages: QuoteMessages | None = None
|
|
636
|
+
quotes: list[Quote] = Field(default_factory=_empty_quotes, alias="quoteData")
|
|
637
|
+
|
|
638
|
+
@model_validator(mode="before")
|
|
639
|
+
@classmethod
|
|
640
|
+
def normalize_response(cls, value: object) -> object:
|
|
641
|
+
if not isinstance(value, dict):
|
|
642
|
+
return value
|
|
643
|
+
data = dict(cast(dict[str, Any], value))
|
|
644
|
+
if "QuoteData" in data and "quoteData" not in data:
|
|
645
|
+
data["quoteData"] = data["QuoteData"]
|
|
646
|
+
data.pop("QuoteData", None)
|
|
647
|
+
if "Messages" in data and "messages" not in data:
|
|
648
|
+
data["messages"] = data["Messages"]
|
|
649
|
+
data.pop("Messages", None)
|
|
650
|
+
return data
|
|
651
|
+
|
|
652
|
+
@field_validator("quotes", mode="before")
|
|
653
|
+
@classmethod
|
|
654
|
+
def normalize_quotes(cls, value: object) -> object:
|
|
655
|
+
if value is None:
|
|
656
|
+
return []
|
|
657
|
+
if isinstance(value, dict):
|
|
658
|
+
return [cast(dict[str, Any], value)]
|
|
659
|
+
return value
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class OptionExpiration(BrokerModel):
|
|
663
|
+
year: int | None = None
|
|
664
|
+
month: int | None = None
|
|
665
|
+
day: int | None = None
|
|
666
|
+
expiry_type: str | None = Field(default=None, alias="expiryType")
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _empty_option_expirations() -> list[OptionExpiration]:
|
|
670
|
+
return []
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class OptionExpirationsResponse(BrokerModel):
|
|
674
|
+
expiration_dates: list[OptionExpiration] = Field(
|
|
675
|
+
default_factory=_empty_option_expirations, alias="expirationDates"
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
@model_validator(mode="before")
|
|
679
|
+
@classmethod
|
|
680
|
+
def normalize_response(cls, value: object) -> object:
|
|
681
|
+
if not isinstance(value, dict):
|
|
682
|
+
return value
|
|
683
|
+
data = dict(cast(dict[str, Any], value))
|
|
684
|
+
for source in ("ExpirationDates", "ExpirationDate", "expirationDate"):
|
|
685
|
+
if source in data and "expirationDates" not in data:
|
|
686
|
+
data["expirationDates"] = data[source]
|
|
687
|
+
data.pop(source, None)
|
|
688
|
+
return data
|
|
689
|
+
|
|
690
|
+
@field_validator("expiration_dates", mode="before")
|
|
691
|
+
@classmethod
|
|
692
|
+
def normalize_expiration_dates(cls, value: object) -> object:
|
|
693
|
+
if value is None:
|
|
694
|
+
return []
|
|
695
|
+
if isinstance(value, dict):
|
|
696
|
+
return [cast(dict[str, Any], value)]
|
|
697
|
+
return value
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class OptionContract(BrokerModel):
|
|
701
|
+
option_category: str | None = Field(default=None, alias="optionCategory")
|
|
702
|
+
option_root_symbol: str | None = Field(default=None, alias="optionRootSymbol")
|
|
703
|
+
time_stamp: datetime | None = Field(default=None, alias="timeStamp")
|
|
704
|
+
adjusted_flag: bool | str | None = Field(default=None, alias="adjustedFlag")
|
|
705
|
+
display_symbol: str | None = Field(default=None, alias="displaySymbol")
|
|
706
|
+
option_type: str | None = Field(default=None, alias="optionType")
|
|
707
|
+
strike_price: Decimal | None = Field(default=None, alias="strikePrice")
|
|
708
|
+
symbol: str | None = None
|
|
709
|
+
bid: Decimal | None = None
|
|
710
|
+
ask: Decimal | None = None
|
|
711
|
+
bid_size: int | None = Field(default=None, alias="bidSize")
|
|
712
|
+
ask_size: int | None = Field(default=None, alias="askSize")
|
|
713
|
+
in_the_money: str | None = Field(default=None, alias="inTheMoney")
|
|
714
|
+
volume: int | None = None
|
|
715
|
+
open_interest: int | None = Field(default=None, alias="openInterest")
|
|
716
|
+
net_change: Decimal | None = Field(default=None, alias="netChange")
|
|
717
|
+
last_price: Decimal | None = Field(default=None, alias="lastPrice")
|
|
718
|
+
quote_detail: str | None = Field(default=None, alias="quoteDetail")
|
|
719
|
+
osi_key: str | None = Field(default=None, alias="osiKey")
|
|
720
|
+
option_greek: OptionGreeks | None = Field(default=None, alias="optionGreek")
|
|
721
|
+
|
|
722
|
+
@field_validator("time_stamp", mode="before")
|
|
723
|
+
@classmethod
|
|
724
|
+
def parse_time_stamp(cls, value: object) -> datetime | None:
|
|
725
|
+
return parse_broker_datetime(value)
|
|
726
|
+
|
|
727
|
+
@model_validator(mode="before")
|
|
728
|
+
@classmethod
|
|
729
|
+
def normalize_nested_aliases(cls, value: object) -> object:
|
|
730
|
+
if not isinstance(value, dict):
|
|
731
|
+
return value
|
|
732
|
+
data = dict(cast(dict[str, Any], value))
|
|
733
|
+
for source in ("OptionGreeks", "optionGreeks"):
|
|
734
|
+
if source in data and "optionGreek" not in data:
|
|
735
|
+
data["optionGreek"] = data[source]
|
|
736
|
+
data.pop(source, None)
|
|
737
|
+
return data
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class OptionChainPair(BrokerModel):
|
|
741
|
+
call: OptionContract | None = None
|
|
742
|
+
put: OptionContract | None = None
|
|
743
|
+
pair_type: str | None = Field(default=None, alias="pairType")
|
|
744
|
+
|
|
745
|
+
@model_validator(mode="before")
|
|
746
|
+
@classmethod
|
|
747
|
+
def normalize_nested_aliases(cls, value: object) -> object:
|
|
748
|
+
if not isinstance(value, dict):
|
|
749
|
+
return value
|
|
750
|
+
data = dict(cast(dict[str, Any], value))
|
|
751
|
+
for source, target in {
|
|
752
|
+
"Call": "call",
|
|
753
|
+
"OptionCall": "call",
|
|
754
|
+
"optioncall": "call",
|
|
755
|
+
"Put": "put",
|
|
756
|
+
"OptionPut": "put",
|
|
757
|
+
"optionPut": "put",
|
|
758
|
+
}.items():
|
|
759
|
+
if source in data and target not in data:
|
|
760
|
+
data[target] = data[source]
|
|
761
|
+
data.pop(source, None)
|
|
762
|
+
return data
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
class SelectedExpiration(BrokerModel):
|
|
766
|
+
month: int | None = None
|
|
767
|
+
year: int | None = None
|
|
768
|
+
day: int | None = None
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _empty_option_pairs() -> list[OptionChainPair]:
|
|
772
|
+
return []
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class OptionChainResponse(BrokerModel):
|
|
776
|
+
option_pairs: list[OptionChainPair] = Field(
|
|
777
|
+
default_factory=_empty_option_pairs, alias="optionPairs"
|
|
778
|
+
)
|
|
779
|
+
time_stamp: datetime | None = Field(default=None, alias="timeStamp")
|
|
780
|
+
quote_type: str | None = Field(default=None, alias="quoteType")
|
|
781
|
+
near_price: Decimal | None = Field(default=None, alias="nearPrice")
|
|
782
|
+
selected: SelectedExpiration | None = None
|
|
783
|
+
|
|
784
|
+
@field_validator("time_stamp", mode="before")
|
|
785
|
+
@classmethod
|
|
786
|
+
def parse_time_stamp(cls, value: object) -> datetime | None:
|
|
787
|
+
return parse_broker_datetime(value)
|
|
788
|
+
|
|
789
|
+
@model_validator(mode="before")
|
|
790
|
+
@classmethod
|
|
791
|
+
def normalize_response(cls, value: object) -> object:
|
|
792
|
+
if not isinstance(value, dict):
|
|
793
|
+
return value
|
|
794
|
+
data = dict(cast(dict[str, Any], value))
|
|
795
|
+
for source in ("OptionPairs", "OptionPair", "optionPair"):
|
|
796
|
+
if source in data and "optionPairs" not in data:
|
|
797
|
+
data["optionPairs"] = data[source]
|
|
798
|
+
data.pop(source, None)
|
|
799
|
+
if "SelectedED" in data and "selected" not in data:
|
|
800
|
+
data["selected"] = data["SelectedED"]
|
|
801
|
+
data.pop("SelectedED", None)
|
|
802
|
+
return data
|
|
803
|
+
|
|
804
|
+
@field_validator("option_pairs", mode="before")
|
|
805
|
+
@classmethod
|
|
806
|
+
def normalize_option_pairs(cls, value: object) -> object:
|
|
807
|
+
if value is None:
|
|
808
|
+
return []
|
|
809
|
+
if isinstance(value, dict):
|
|
810
|
+
return [cast(dict[str, Any], value)]
|
|
811
|
+
return value
|