afp-sdk 0.5.3__py3-none-any.whl → 0.6.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.
- afp/__init__.py +4 -1
- afp/afp.py +11 -0
- afp/api/admin.py +1 -1
- afp/api/base.py +11 -2
- afp/api/margin_account.py +12 -148
- afp/api/product.py +302 -148
- afp/api/trading.py +19 -22
- afp/auth.py +14 -0
- afp/bindings/__init__.py +30 -16
- afp/bindings/admin_facet.py +890 -0
- afp/bindings/clearing_facet.py +356 -773
- afp/bindings/facade.py +9 -6
- afp/bindings/final_settlement_facet.py +258 -99
- afp/bindings/margin_account.py +524 -839
- afp/bindings/margin_account_facet.py +722 -0
- afp/bindings/margin_account_registry.py +184 -310
- afp/bindings/mark_price_tracker_facet.py +74 -16
- afp/bindings/product_registry.py +1577 -541
- afp/bindings/product_registry_facet.py +1467 -0
- afp/bindings/system_viewer.py +592 -369
- afp/bindings/types.py +223 -0
- afp/config.py +4 -0
- afp/constants.py +49 -6
- afp/decorators.py +25 -3
- afp/dtos.py +142 -0
- afp/exceptions.py +14 -0
- afp/exchange.py +13 -8
- afp/hashing.py +7 -5
- afp/ipfs.py +245 -0
- afp/json-schemas/bafyreiaw34o6l3rmatabzbds2i2myazdw2yolevcpsoyd2i2g3ms7wa2eq.json +1 -0
- afp/json-schemas/bafyreibnfg6nq74dvpkre5rakkccij7iadp5rxpim7omsatjnrpmj3y7v4.json +1 -0
- afp/json-schemas/bafyreicgr6dfo5yduixjkcifghiulskfegwojvuwodtouvivl362zndhxe.json +1 -0
- afp/json-schemas/bafyreicheoypx6synljushh7mq2572iyhlolf4nake2p5dwobgnj3r5eua.json +1 -0
- afp/json-schemas/bafyreid35a67db4sqh4fs6boddyt2xvscbqy6nqvsp5jjur56qhkw4ixre.json +1 -0
- afp/json-schemas/bafyreidzs7okcpqiss6ztftltyptqwnw5e5opsy5yntospekjha4kpykaa.json +1 -0
- afp/json-schemas/bafyreifcec2km7hxwq6oqzjlspni2mgipetjb7pqtaewh2efislzoctboi.json +1 -0
- afp/json-schemas/bafyreihn3oiaxffe4e2w7pwtreadpw3obfd7gqlogbcxm56jc2hzfvco74.json +1 -0
- afp/json-schemas/bafyreihur3dzwhja6uxsbcw6eeoj3xmmc4e3zkmyzpot5v5dleevxe5zam.json +1 -0
- afp/schemas.py +228 -176
- afp/types.py +169 -0
- afp/validators.py +218 -8
- {afp_sdk-0.5.3.dist-info → afp_sdk-0.6.0.dist-info}/METADATA +73 -10
- afp_sdk-0.6.0.dist-info/RECORD +50 -0
- afp/bindings/auctioneer_facet.py +0 -752
- afp/bindings/bankruptcy_facet.py +0 -391
- afp/bindings/trading_protocol.py +0 -1158
- afp_sdk-0.5.3.dist-info/RECORD +0 -37
- {afp_sdk-0.5.3.dist-info → afp_sdk-0.6.0.dist-info}/WHEEL +0 -0
- {afp_sdk-0.5.3.dist-info → afp_sdk-0.6.0.dist-info}/licenses/LICENSE +0 -0
afp/schemas.py
CHANGED
|
@@ -1,127 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
from decimal import Decimal
|
|
3
|
-
from functools import partial
|
|
4
|
-
from typing import Annotated, Any, Literal
|
|
5
|
-
|
|
6
|
-
import inflection
|
|
7
|
-
from pydantic import (
|
|
8
|
-
AfterValidator,
|
|
9
|
-
AliasGenerator,
|
|
10
|
-
BaseModel,
|
|
11
|
-
BeforeValidator,
|
|
12
|
-
ConfigDict,
|
|
13
|
-
Field,
|
|
14
|
-
PlainSerializer,
|
|
15
|
-
computed_field,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
from . import validators
|
|
19
|
-
from .enums import ListingState, OrderSide, OrderState, OrderType, TradeState
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# Use datetime internally but UNIX timestamp in client-server communication
|
|
23
|
-
Timestamp = Annotated[
|
|
24
|
-
datetime,
|
|
25
|
-
BeforeValidator(validators.ensure_datetime),
|
|
26
|
-
PlainSerializer(validators.ensure_timestamp, return_type=int, when_used="json"),
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class Model(BaseModel):
|
|
31
|
-
model_config = ConfigDict(
|
|
32
|
-
alias_generator=AliasGenerator(
|
|
33
|
-
alias=partial(inflection.camelize, uppercase_first_letter=False),
|
|
34
|
-
),
|
|
35
|
-
frozen=True,
|
|
36
|
-
populate_by_name=True,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
# Change the default value of by_alias to True
|
|
40
|
-
def model_dump_json(self, by_alias: bool = True, **kwargs: Any) -> str:
|
|
41
|
-
return super().model_dump_json(by_alias=by_alias, **kwargs)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class PaginationFilter(Model):
|
|
45
|
-
batch: Annotated[None | int, Field(gt=0, exclude=True)]
|
|
46
|
-
batch_size: Annotated[None | int, Field(gt=0, exclude=True)]
|
|
47
|
-
newest_first: Annotated[None | bool, Field(exclude=True)]
|
|
48
|
-
|
|
49
|
-
@computed_field
|
|
50
|
-
@property
|
|
51
|
-
def page(self) -> None | int:
|
|
52
|
-
return self.batch
|
|
53
|
-
|
|
54
|
-
@computed_field
|
|
55
|
-
@property
|
|
56
|
-
def page_size(self) -> None | int:
|
|
57
|
-
return self.batch_size
|
|
58
|
-
|
|
59
|
-
@computed_field
|
|
60
|
-
@property
|
|
61
|
-
def sort(self) -> None | Literal["ASC", "DESC"]:
|
|
62
|
-
match self.newest_first:
|
|
63
|
-
case None:
|
|
64
|
-
return None
|
|
65
|
-
case True:
|
|
66
|
-
return "DESC"
|
|
67
|
-
case False:
|
|
68
|
-
return "ASC"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
# Authentication
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class LoginSubmission(Model):
|
|
75
|
-
message: str
|
|
76
|
-
signature: str
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class ExchangeParameters(Model):
|
|
80
|
-
trading_protocol_id: str
|
|
81
|
-
trading_fee_rate: Decimal
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# Admin API
|
|
1
|
+
"""AFP data structures."""
|
|
85
2
|
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Annotated, Any, ClassVar, Literal, Self
|
|
86
5
|
|
|
87
|
-
|
|
88
|
-
id: Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
89
|
-
|
|
6
|
+
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
90
7
|
|
|
91
|
-
|
|
92
|
-
|
|
8
|
+
from . import constants, validators
|
|
9
|
+
from .constants import schema_cids
|
|
10
|
+
from .enums import ListingState, OrderSide, OrderState, OrderType, TradeState
|
|
11
|
+
from .types import (
|
|
12
|
+
CID,
|
|
13
|
+
URL,
|
|
14
|
+
AliasedModel,
|
|
15
|
+
ISODate,
|
|
16
|
+
ISODateTime,
|
|
17
|
+
Model,
|
|
18
|
+
PinnedModel,
|
|
19
|
+
Timestamp,
|
|
20
|
+
)
|
|
93
21
|
|
|
94
22
|
|
|
95
23
|
# Trading API
|
|
96
24
|
|
|
97
25
|
|
|
98
|
-
class ExchangeProduct(
|
|
26
|
+
class ExchangeProduct(AliasedModel):
|
|
99
27
|
id: str
|
|
100
|
-
symbol: str
|
|
28
|
+
symbol: Annotated[str, AfterValidator(validators.validate_all_caps)]
|
|
101
29
|
tick_size: int
|
|
102
30
|
collateral_asset: str
|
|
103
31
|
listing_state: ListingState
|
|
32
|
+
min_price: Decimal
|
|
33
|
+
max_price: Decimal
|
|
104
34
|
|
|
105
35
|
def __str__(self) -> str:
|
|
106
36
|
return self.id
|
|
107
37
|
|
|
108
38
|
|
|
109
|
-
class
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class IntentData(Model):
|
|
39
|
+
class IntentData(AliasedModel):
|
|
114
40
|
trading_protocol_id: str
|
|
115
41
|
product_id: str
|
|
116
|
-
limit_price:
|
|
42
|
+
limit_price: Decimal
|
|
117
43
|
quantity: Annotated[int, Field(gt=0)]
|
|
118
|
-
max_trading_fee_rate: Annotated[
|
|
44
|
+
max_trading_fee_rate: Annotated[
|
|
45
|
+
Decimal,
|
|
46
|
+
Field(le=Decimal((2**32 - 1) / constants.FEE_RATE_MULTIPLIER)), # uint32
|
|
47
|
+
]
|
|
119
48
|
side: OrderSide
|
|
120
49
|
good_until_time: Timestamp
|
|
121
50
|
nonce: int
|
|
51
|
+
referral: Annotated[str, AfterValidator(validators.validate_address)]
|
|
122
52
|
|
|
123
53
|
|
|
124
|
-
class Intent(
|
|
54
|
+
class Intent(AliasedModel):
|
|
125
55
|
hash: str
|
|
126
56
|
margin_account_id: str
|
|
127
57
|
intent_account_id: str
|
|
@@ -129,7 +59,7 @@ class Intent(Model):
|
|
|
129
59
|
data: IntentData
|
|
130
60
|
|
|
131
61
|
|
|
132
|
-
class Order(
|
|
62
|
+
class Order(AliasedModel):
|
|
133
63
|
id: str
|
|
134
64
|
type: OrderType
|
|
135
65
|
timestamp: Timestamp
|
|
@@ -138,35 +68,14 @@ class Order(Model):
|
|
|
138
68
|
intent: Intent
|
|
139
69
|
|
|
140
70
|
|
|
141
|
-
class
|
|
142
|
-
intent_account_id: str
|
|
143
|
-
product_id: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
144
|
-
type: None | OrderType
|
|
145
|
-
states: Annotated[list[OrderState], Field(exclude=True)]
|
|
146
|
-
side: None | OrderSide
|
|
147
|
-
start: None | Timestamp
|
|
148
|
-
end: None | Timestamp
|
|
149
|
-
|
|
150
|
-
@computed_field
|
|
151
|
-
@property
|
|
152
|
-
def state(self) -> str | None:
|
|
153
|
-
return ",".join(self.states) if self.states else None
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class OrderCancellationData(Model):
|
|
71
|
+
class OrderCancellationData(AliasedModel):
|
|
157
72
|
intent_hash: Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
158
73
|
nonce: int
|
|
159
74
|
intent_account_id: str
|
|
160
75
|
signature: str
|
|
161
76
|
|
|
162
77
|
|
|
163
|
-
class
|
|
164
|
-
type: OrderType
|
|
165
|
-
intent: Intent | None = None
|
|
166
|
-
cancellation_data: OrderCancellationData | None = None
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class Trade(Model):
|
|
78
|
+
class Trade(AliasedModel):
|
|
170
79
|
# Convert ID from int to str for backward compatibility
|
|
171
80
|
id: Annotated[str, BeforeValidator(str)]
|
|
172
81
|
product_id: str
|
|
@@ -177,39 +86,26 @@ class Trade(Model):
|
|
|
177
86
|
rejection_reason: str | None
|
|
178
87
|
|
|
179
88
|
|
|
180
|
-
class OrderFill(
|
|
89
|
+
class OrderFill(AliasedModel):
|
|
181
90
|
order: Order
|
|
182
91
|
trade: Trade
|
|
183
92
|
quantity: int
|
|
184
93
|
price: Decimal
|
|
94
|
+
trading_fee_rate: Decimal
|
|
185
95
|
|
|
186
96
|
|
|
187
|
-
class
|
|
188
|
-
intent_account_id: str
|
|
189
|
-
product_id: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
190
|
-
intent_hash: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
191
|
-
start: None | Timestamp
|
|
192
|
-
end: None | Timestamp
|
|
193
|
-
trade_states: Annotated[list[TradeState], Field(exclude=True)]
|
|
194
|
-
|
|
195
|
-
@computed_field
|
|
196
|
-
@property
|
|
197
|
-
def trade_state(self) -> str | None:
|
|
198
|
-
return ",".join(self.trade_states) if self.trade_states else None
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
class MarketDepthItem(Model):
|
|
97
|
+
class MarketDepthItem(AliasedModel):
|
|
202
98
|
price: Decimal
|
|
203
99
|
quantity: int
|
|
204
100
|
|
|
205
101
|
|
|
206
|
-
class MarketDepthData(
|
|
102
|
+
class MarketDepthData(AliasedModel):
|
|
207
103
|
product_id: str
|
|
208
104
|
bids: list[MarketDepthItem]
|
|
209
105
|
asks: list[MarketDepthItem]
|
|
210
106
|
|
|
211
107
|
|
|
212
|
-
class OHLCVItem(
|
|
108
|
+
class OHLCVItem(AliasedModel):
|
|
213
109
|
timestamp: Timestamp
|
|
214
110
|
open: Decimal
|
|
215
111
|
high: Decimal
|
|
@@ -218,7 +114,7 @@ class OHLCVItem(Model):
|
|
|
218
114
|
volume: int
|
|
219
115
|
|
|
220
116
|
|
|
221
|
-
#
|
|
117
|
+
# Margin Account API
|
|
222
118
|
|
|
223
119
|
|
|
224
120
|
class Transaction(Model):
|
|
@@ -235,51 +131,207 @@ class Position(Model):
|
|
|
235
131
|
pnl: Decimal
|
|
236
132
|
|
|
237
133
|
|
|
238
|
-
#
|
|
134
|
+
# Product API
|
|
239
135
|
|
|
240
136
|
|
|
241
|
-
class
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
137
|
+
class ExpirySpecification(AliasedModel):
|
|
138
|
+
earliest_fsp_submission_time: Annotated[
|
|
139
|
+
ISODateTime, Field(alias="earliestFSPSubmissionTime")
|
|
140
|
+
]
|
|
141
|
+
tradeout_interval: Annotated[int, Field(ge=0)]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class OracleSpecification(AliasedModel):
|
|
248
145
|
oracle_address: Annotated[str, AfterValidator(validators.validate_address)]
|
|
249
|
-
fsv_decimals: Annotated[int, Field(ge=0,
|
|
146
|
+
fsv_decimals: Annotated[int, Field(ge=0, le=255)] # uint8
|
|
250
147
|
fsp_alpha: Decimal
|
|
251
148
|
fsp_beta: Decimal
|
|
252
149
|
fsv_calldata: Annotated[str, AfterValidator(validators.validate_hexstr)]
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ProductMetadata(AliasedModel):
|
|
153
|
+
builder: Annotated[str, AfterValidator(validators.validate_address)]
|
|
154
|
+
symbol: Annotated[
|
|
155
|
+
str,
|
|
156
|
+
Field(pattern=r"^[A-Z0-9]{1,16}$", min_length=1, max_length=16),
|
|
157
|
+
]
|
|
158
|
+
description: str
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class BaseProduct(AliasedModel):
|
|
162
|
+
metadata: ProductMetadata
|
|
163
|
+
oracle_spec: OracleSpecification
|
|
256
164
|
collateral_asset: Annotated[str, AfterValidator(validators.validate_address)]
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
maintenance_margin_requirement: Annotated[Decimal, Field(gt=0)]
|
|
262
|
-
auction_bounty: Annotated[Decimal, Field(ge=0, le=1)]
|
|
263
|
-
tradeout_interval: Annotated[int, Field(ge=0)]
|
|
264
|
-
extended_metadata: str
|
|
165
|
+
start_time: ISODateTime
|
|
166
|
+
point_value: Decimal
|
|
167
|
+
price_decimals: Annotated[int, Field(ge=0, le=255)] # uint8
|
|
168
|
+
extended_metadata: CID | None = None
|
|
265
169
|
|
|
266
|
-
def __str__(self) -> str:
|
|
267
|
-
return self.id
|
|
268
170
|
|
|
171
|
+
class PredictionProductV1(AliasedModel):
|
|
172
|
+
base: BaseProduct
|
|
173
|
+
expiry_spec: ExpirySpecification
|
|
174
|
+
min_price: Decimal
|
|
175
|
+
max_price: Decimal
|
|
269
176
|
|
|
270
|
-
|
|
177
|
+
@model_validator(mode="after")
|
|
178
|
+
def _cross_validate(self) -> Self:
|
|
179
|
+
validators.validate_price_limits(self.min_price, self.max_price)
|
|
180
|
+
validators.validate_time_limits(
|
|
181
|
+
self.base.start_time, self.expiry_spec.earliest_fsp_submission_time
|
|
182
|
+
)
|
|
183
|
+
return self
|
|
271
184
|
|
|
272
185
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
186
|
+
# Extended metadata schemas
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ApiSpec(Model):
|
|
190
|
+
standard: Literal["JSONPath", "GraphQL"]
|
|
191
|
+
spec_variant: Literal["underlying-history", "product-fsv"] | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ApiSpecJSONPath(ApiSpec):
|
|
195
|
+
standard: Literal["JSONPath"] = "JSONPath" # type: ignore
|
|
196
|
+
url: URL
|
|
197
|
+
date_path: str
|
|
198
|
+
value_path: str
|
|
199
|
+
auth_param_location: Literal["query", "header", "none"] = "none"
|
|
200
|
+
auth_param_name: str | None = None
|
|
201
|
+
auth_param_prefix: str | None = None
|
|
202
|
+
continuation_token_param: str | None = None
|
|
203
|
+
continuation_token_path: str | None = None
|
|
204
|
+
date_format_custom: str | None = None
|
|
205
|
+
date_format_type: Literal["iso_8601", "unix_timestamp", "custom"] = "iso_8601"
|
|
206
|
+
headers: dict[str, str] | None = None
|
|
207
|
+
max_pages: Annotated[int | None, Field(ge=1)] = 10
|
|
208
|
+
timestamp_scale: Annotated[int | float, Field(ge=1)] = 1
|
|
209
|
+
timezone: Annotated[
|
|
210
|
+
str,
|
|
211
|
+
Field(pattern=r"^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z][A-Za-z0-9_+-]*)*$"),
|
|
212
|
+
] = "UTC"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class BaseCaseResolution(Model):
|
|
216
|
+
condition: Annotated[str, Field(min_length=1)]
|
|
217
|
+
fsp_resolution: Annotated[str, Field(min_length=1)]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class EdgeCase(Model):
|
|
221
|
+
condition: Annotated[str, Field(min_length=1)]
|
|
222
|
+
fsp_resolution: Annotated[str, Field(min_length=1)]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class OutcomeSpace(PinnedModel):
|
|
226
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_SPACE_V020
|
|
227
|
+
|
|
228
|
+
fsp_type: Literal["scalar", "binary", "ternary"]
|
|
229
|
+
description: Annotated[str, Field(min_length=1)]
|
|
230
|
+
base_case: BaseCaseResolution
|
|
231
|
+
edge_cases: Annotated[list[EdgeCase], Field(default_factory=list)]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class OutcomeSpaceScalar(OutcomeSpace):
|
|
235
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_SPACE_SCALAR_V020
|
|
236
|
+
|
|
237
|
+
fsp_type: Literal["scalar"] = "scalar" # type: ignore
|
|
238
|
+
units: Annotated[str, Field(min_length=1)]
|
|
239
|
+
source_name: Annotated[str, Field(min_length=1)]
|
|
240
|
+
source_uri: URL
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class OutcomeSpaceTimeSeries(OutcomeSpaceScalar):
|
|
244
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_SPACE_TIME_SERIES_V020
|
|
245
|
+
|
|
246
|
+
frequency: Literal[
|
|
247
|
+
"daily",
|
|
248
|
+
"weekly",
|
|
249
|
+
"fortnightly",
|
|
250
|
+
"semimonthly",
|
|
251
|
+
"monthly",
|
|
252
|
+
"quarterly",
|
|
253
|
+
"yearly",
|
|
254
|
+
]
|
|
255
|
+
history_api_spec: ApiSpecJSONPath | ApiSpec | None = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class TemporalObservation(Model):
|
|
259
|
+
reference_date: ISODate
|
|
260
|
+
release_date: ISODate
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class OutcomePoint(PinnedModel):
|
|
264
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_POINT_V020
|
|
265
|
+
|
|
266
|
+
fsp_type: Literal["scalar", "binary", "ternary"]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class OutcomePointTimeSeries(OutcomePoint):
|
|
270
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_POINT_TIME_SERIES_V020
|
|
271
|
+
|
|
272
|
+
fsp_type: Literal["scalar"] = "scalar" # type: ignore
|
|
273
|
+
observation: TemporalObservation
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class OutcomePointEvent(OutcomePoint):
|
|
277
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_POINT_EVENT_V020
|
|
278
|
+
|
|
279
|
+
fsp_type: Literal["binary", "ternary"] # type: ignore
|
|
280
|
+
outcome: Annotated[str, Field(min_length=1)]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class OracleConfig(PinnedModel):
|
|
284
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.ORACLE_CONFIG_V020
|
|
285
|
+
|
|
286
|
+
description: Annotated[str, Field(min_length=1)]
|
|
287
|
+
project_url: URL | None = None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class OracleConfigPrototype1(OracleConfig):
|
|
291
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.ORACLE_CONFIG_PROTOTYPE1_V020
|
|
292
|
+
|
|
293
|
+
evaluation_api_spec: ApiSpecJSONPath | ApiSpec
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class OracleFallback(PinnedModel):
|
|
297
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.ORACLE_FALLBACK_V020
|
|
298
|
+
|
|
299
|
+
fallback_time: ISODateTime
|
|
300
|
+
fallback_fsp: Decimal
|
|
301
|
+
|
|
278
302
|
|
|
303
|
+
class PredictionProduct(Model):
|
|
304
|
+
product: PredictionProductV1
|
|
305
|
+
outcome_space: OutcomeSpaceTimeSeries | OutcomeSpaceScalar | OutcomeSpace
|
|
306
|
+
outcome_point: OutcomePointEvent | OutcomePointTimeSeries | OutcomePoint
|
|
307
|
+
oracle_config: OracleConfigPrototype1 | OracleConfig
|
|
308
|
+
oracle_fallback: OracleFallback
|
|
279
309
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
@model_validator(mode="after")
|
|
311
|
+
def _cross_validate(self) -> Self:
|
|
312
|
+
validators.validate_matching_fsp_types(
|
|
313
|
+
self.outcome_space.fsp_type, self.outcome_point.fsp_type
|
|
314
|
+
)
|
|
315
|
+
validators.validate_oracle_fallback_time(
|
|
316
|
+
self.oracle_fallback.fallback_time,
|
|
317
|
+
self.product.expiry_spec.earliest_fsp_submission_time,
|
|
318
|
+
)
|
|
319
|
+
validators.validate_oracle_fallback_fsp(
|
|
320
|
+
self.oracle_fallback.fallback_fsp,
|
|
321
|
+
self.product.min_price,
|
|
322
|
+
self.product.max_price,
|
|
323
|
+
)
|
|
324
|
+
validators.validate_outcome_space_conditions(
|
|
325
|
+
self.outcome_space.base_case.condition,
|
|
326
|
+
[case.condition for case in self.outcome_space.edge_cases],
|
|
327
|
+
self.outcome_point.model_dump(),
|
|
328
|
+
)
|
|
329
|
+
if isinstance(self.outcome_space, OutcomeSpaceTimeSeries) and isinstance(
|
|
330
|
+
self.outcome_point, OutcomePointTimeSeries
|
|
331
|
+
):
|
|
332
|
+
validators.validate_symbol(
|
|
333
|
+
self.product.base.metadata.symbol,
|
|
334
|
+
self.outcome_space.frequency,
|
|
335
|
+
self.outcome_point.observation.release_date,
|
|
336
|
+
)
|
|
337
|
+
return self
|
afp/types.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from datetime import date, datetime, UTC
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Annotated, Any, ClassVar, Self
|
|
4
|
+
|
|
5
|
+
import inflection
|
|
6
|
+
import multiformats
|
|
7
|
+
import rfc8785
|
|
8
|
+
from pydantic import (
|
|
9
|
+
AfterValidator,
|
|
10
|
+
AliasGenerator,
|
|
11
|
+
BaseModel,
|
|
12
|
+
BeforeValidator,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
PlainSerializer,
|
|
16
|
+
model_validator,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from . import constants, validators
|
|
20
|
+
|
|
21
|
+
CID_MODEL_MAP: dict[str, type["PinnedModel"]] = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Conversions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def ensure_py_datetime(value: datetime | int | float | str) -> datetime:
|
|
28
|
+
if isinstance(value, datetime):
|
|
29
|
+
return value.astimezone(UTC)
|
|
30
|
+
if isinstance(value, int) or isinstance(value, float):
|
|
31
|
+
return datetime.fromtimestamp(value, UTC)
|
|
32
|
+
return datetime.fromisoformat(value).astimezone(UTC)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ensure_py_date(value: date | str) -> date:
|
|
36
|
+
if isinstance(value, date):
|
|
37
|
+
return value
|
|
38
|
+
return date.fromisoformat(value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ensure_timestamp(value: datetime) -> int:
|
|
42
|
+
return int(value.timestamp())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ensure_iso_datetime(value: datetime) -> str:
|
|
46
|
+
assert value.tzinfo is UTC, f"{value} should be in UTC timezone"
|
|
47
|
+
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_iso_date(value: date) -> str:
|
|
51
|
+
return value.isoformat()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Custom types
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
URL = Annotated[
|
|
58
|
+
str,
|
|
59
|
+
Field(min_length=1, max_length=2083),
|
|
60
|
+
BeforeValidator(validators.validate_url),
|
|
61
|
+
AfterValidator(validators.verify_url),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
CID = Annotated[
|
|
65
|
+
str,
|
|
66
|
+
Field(
|
|
67
|
+
pattern=r"^(Qm[1-9A-HJ-NP-Za-km-z]{44}|b[a-z2-7]{58,}|z[1-9A-HJ-NP-Za-km-z]{48,})$"
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Decode CIDs for serialization so that the DAG-CBOR encoder will convert them
|
|
72
|
+
# into IPLD Link format
|
|
73
|
+
IPLD_LINK = Annotated[
|
|
74
|
+
CID,
|
|
75
|
+
BeforeValidator( # Ensure the same encoding is used consistently
|
|
76
|
+
lambda cid: multiformats.CID.decode(str(cid)).encode(
|
|
77
|
+
constants.IPFS_CID_ENCODING
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
PlainSerializer(multiformats.CID.decode, return_type=multiformats.CID),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Use datetime internally but UNIX timestamp in serialized format
|
|
84
|
+
Timestamp = Annotated[
|
|
85
|
+
datetime,
|
|
86
|
+
BeforeValidator(ensure_py_datetime),
|
|
87
|
+
AfterValidator(validators.validate_non_negative_timestamp),
|
|
88
|
+
PlainSerializer(ensure_timestamp, return_type=int),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# Use datetime internally but ISO string in serialized format
|
|
92
|
+
ISODateTime = Annotated[
|
|
93
|
+
datetime,
|
|
94
|
+
BeforeValidator(ensure_py_datetime),
|
|
95
|
+
AfterValidator(validators.validate_non_negative_timestamp),
|
|
96
|
+
PlainSerializer(ensure_iso_datetime, return_type=str),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Use date internally but ISO string in serialized format
|
|
100
|
+
ISODate = Annotated[
|
|
101
|
+
date,
|
|
102
|
+
BeforeValidator(ensure_py_date),
|
|
103
|
+
PlainSerializer(ensure_iso_date, return_type=str),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Base models
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Model(BaseModel):
|
|
111
|
+
"""Base immutable schema."""
|
|
112
|
+
|
|
113
|
+
model_config = ConfigDict(frozen=True)
|
|
114
|
+
|
|
115
|
+
# Always serialize/deserialize by alias
|
|
116
|
+
|
|
117
|
+
def model_dump(self, by_alias: bool = True, **kwargs: Any) -> dict[Any, Any]:
|
|
118
|
+
return super().model_dump(by_alias=by_alias, **kwargs)
|
|
119
|
+
|
|
120
|
+
def model_dump_json(self, by_alias: bool = True, **kwargs: Any) -> str:
|
|
121
|
+
return super().model_dump_json(by_alias=by_alias, **kwargs)
|
|
122
|
+
|
|
123
|
+
def model_dump_canonical_json(self, **kwargs: Any) -> str:
|
|
124
|
+
obj = self.model_dump(mode="json", **kwargs)
|
|
125
|
+
return rfc8785.dumps(obj).decode("utf-8")
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def model_validate(cls, *args: Any, by_alias: bool = True, **kwargs: Any) -> Self:
|
|
129
|
+
return super().model_validate(*args, by_alias=by_alias, **kwargs)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def model_validate_json(
|
|
133
|
+
cls, *args: Any, by_alias: bool = True, **kwargs: Any
|
|
134
|
+
) -> Self:
|
|
135
|
+
return super().model_validate_json(*args, by_alias=by_alias, **kwargs)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AliasedModel(Model):
|
|
139
|
+
"""Schema that converts property names from snake case to camel case for
|
|
140
|
+
serialization.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
model_config = Model.model_config | ConfigDict(
|
|
144
|
+
alias_generator=AliasGenerator(
|
|
145
|
+
alias=partial(inflection.camelize, uppercase_first_letter=False),
|
|
146
|
+
),
|
|
147
|
+
populate_by_name=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class PinnedModel(Model):
|
|
152
|
+
"""Extended metadata schema that has an IPFS CID."""
|
|
153
|
+
|
|
154
|
+
SCHEMA_CID: ClassVar[CID]
|
|
155
|
+
|
|
156
|
+
def __init_subclass__(cls, **kwargs: Any):
|
|
157
|
+
super().__init_subclass__(**kwargs)
|
|
158
|
+
if "SCHEMA_CID" in cls.__dict__:
|
|
159
|
+
assert cls.SCHEMA_CID not in CID_MODEL_MAP, (
|
|
160
|
+
f"{cls.__name__} model does not have unique CID"
|
|
161
|
+
)
|
|
162
|
+
CID_MODEL_MAP[cls.SCHEMA_CID] = cls
|
|
163
|
+
|
|
164
|
+
@model_validator(mode="after")
|
|
165
|
+
def _ensure_schema_cid(self) -> Self:
|
|
166
|
+
assert "SCHEMA_CID" in self.__class__.__dict__, (
|
|
167
|
+
f"SCHEMA_CID is missing from {self.__class__.__name__} schema"
|
|
168
|
+
)
|
|
169
|
+
return self
|