afp-sdk 0.5.4__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 +10 -19
- 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 +10 -0
- afp/exchange.py +10 -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 +227 -177
- afp/types.py +169 -0
- afp/validators.py +218 -8
- {afp_sdk-0.5.4.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.4.dist-info/RECORD +0 -37
- {afp_sdk-0.5.4.dist-info → afp_sdk-0.6.0.dist-info}/WHEEL +0 -0
- {afp_sdk-0.5.4.dist-info → afp_sdk-0.6.0.dist-info}/licenses/LICENSE +0 -0
afp/schemas.py
CHANGED
|
@@ -1,128 +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
|
-
maker_trading_fee_rate: Decimal
|
|
82
|
-
taker_trading_fee_rate: Decimal
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Admin API
|
|
1
|
+
"""AFP data structures."""
|
|
86
2
|
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Annotated, Any, ClassVar, Literal, Self
|
|
87
5
|
|
|
88
|
-
|
|
89
|
-
id: Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
90
|
-
|
|
6
|
+
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
91
7
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
)
|
|
94
21
|
|
|
95
22
|
|
|
96
23
|
# Trading API
|
|
97
24
|
|
|
98
25
|
|
|
99
|
-
class ExchangeProduct(
|
|
26
|
+
class ExchangeProduct(AliasedModel):
|
|
100
27
|
id: str
|
|
101
|
-
symbol: str
|
|
28
|
+
symbol: Annotated[str, AfterValidator(validators.validate_all_caps)]
|
|
102
29
|
tick_size: int
|
|
103
30
|
collateral_asset: str
|
|
104
31
|
listing_state: ListingState
|
|
32
|
+
min_price: Decimal
|
|
33
|
+
max_price: Decimal
|
|
105
34
|
|
|
106
35
|
def __str__(self) -> str:
|
|
107
36
|
return self.id
|
|
108
37
|
|
|
109
38
|
|
|
110
|
-
class
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class IntentData(Model):
|
|
39
|
+
class IntentData(AliasedModel):
|
|
115
40
|
trading_protocol_id: str
|
|
116
41
|
product_id: str
|
|
117
|
-
limit_price:
|
|
42
|
+
limit_price: Decimal
|
|
118
43
|
quantity: Annotated[int, Field(gt=0)]
|
|
119
|
-
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
|
+
]
|
|
120
48
|
side: OrderSide
|
|
121
49
|
good_until_time: Timestamp
|
|
122
50
|
nonce: int
|
|
51
|
+
referral: Annotated[str, AfterValidator(validators.validate_address)]
|
|
123
52
|
|
|
124
53
|
|
|
125
|
-
class Intent(
|
|
54
|
+
class Intent(AliasedModel):
|
|
126
55
|
hash: str
|
|
127
56
|
margin_account_id: str
|
|
128
57
|
intent_account_id: str
|
|
@@ -130,7 +59,7 @@ class Intent(Model):
|
|
|
130
59
|
data: IntentData
|
|
131
60
|
|
|
132
61
|
|
|
133
|
-
class Order(
|
|
62
|
+
class Order(AliasedModel):
|
|
134
63
|
id: str
|
|
135
64
|
type: OrderType
|
|
136
65
|
timestamp: Timestamp
|
|
@@ -139,35 +68,14 @@ class Order(Model):
|
|
|
139
68
|
intent: Intent
|
|
140
69
|
|
|
141
70
|
|
|
142
|
-
class
|
|
143
|
-
intent_account_id: str
|
|
144
|
-
product_id: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
145
|
-
type: None | OrderType
|
|
146
|
-
states: Annotated[list[OrderState], Field(exclude=True)]
|
|
147
|
-
side: None | OrderSide
|
|
148
|
-
start: None | Timestamp
|
|
149
|
-
end: None | Timestamp
|
|
150
|
-
|
|
151
|
-
@computed_field
|
|
152
|
-
@property
|
|
153
|
-
def state(self) -> str | None:
|
|
154
|
-
return ",".join(self.states) if self.states else None
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class OrderCancellationData(Model):
|
|
71
|
+
class OrderCancellationData(AliasedModel):
|
|
158
72
|
intent_hash: Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
159
73
|
nonce: int
|
|
160
74
|
intent_account_id: str
|
|
161
75
|
signature: str
|
|
162
76
|
|
|
163
77
|
|
|
164
|
-
class
|
|
165
|
-
type: OrderType
|
|
166
|
-
intent: Intent | None = None
|
|
167
|
-
cancellation_data: OrderCancellationData | None = None
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
class Trade(Model):
|
|
78
|
+
class Trade(AliasedModel):
|
|
171
79
|
# Convert ID from int to str for backward compatibility
|
|
172
80
|
id: Annotated[str, BeforeValidator(str)]
|
|
173
81
|
product_id: str
|
|
@@ -178,7 +86,7 @@ class Trade(Model):
|
|
|
178
86
|
rejection_reason: str | None
|
|
179
87
|
|
|
180
88
|
|
|
181
|
-
class OrderFill(
|
|
89
|
+
class OrderFill(AliasedModel):
|
|
182
90
|
order: Order
|
|
183
91
|
trade: Trade
|
|
184
92
|
quantity: int
|
|
@@ -186,32 +94,18 @@ class OrderFill(Model):
|
|
|
186
94
|
trading_fee_rate: Decimal
|
|
187
95
|
|
|
188
96
|
|
|
189
|
-
class
|
|
190
|
-
intent_account_id: str
|
|
191
|
-
product_id: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
192
|
-
intent_hash: None | Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
193
|
-
start: None | Timestamp
|
|
194
|
-
end: None | Timestamp
|
|
195
|
-
trade_states: Annotated[list[TradeState], Field(exclude=True)]
|
|
196
|
-
|
|
197
|
-
@computed_field
|
|
198
|
-
@property
|
|
199
|
-
def trade_state(self) -> str | None:
|
|
200
|
-
return ",".join(self.trade_states) if self.trade_states else None
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
class MarketDepthItem(Model):
|
|
97
|
+
class MarketDepthItem(AliasedModel):
|
|
204
98
|
price: Decimal
|
|
205
99
|
quantity: int
|
|
206
100
|
|
|
207
101
|
|
|
208
|
-
class MarketDepthData(
|
|
102
|
+
class MarketDepthData(AliasedModel):
|
|
209
103
|
product_id: str
|
|
210
104
|
bids: list[MarketDepthItem]
|
|
211
105
|
asks: list[MarketDepthItem]
|
|
212
106
|
|
|
213
107
|
|
|
214
|
-
class OHLCVItem(
|
|
108
|
+
class OHLCVItem(AliasedModel):
|
|
215
109
|
timestamp: Timestamp
|
|
216
110
|
open: Decimal
|
|
217
111
|
high: Decimal
|
|
@@ -220,7 +114,7 @@ class OHLCVItem(Model):
|
|
|
220
114
|
volume: int
|
|
221
115
|
|
|
222
116
|
|
|
223
|
-
#
|
|
117
|
+
# Margin Account API
|
|
224
118
|
|
|
225
119
|
|
|
226
120
|
class Transaction(Model):
|
|
@@ -237,51 +131,207 @@ class Position(Model):
|
|
|
237
131
|
pnl: Decimal
|
|
238
132
|
|
|
239
133
|
|
|
240
|
-
#
|
|
134
|
+
# Product API
|
|
241
135
|
|
|
242
136
|
|
|
243
|
-
class
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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):
|
|
250
145
|
oracle_address: Annotated[str, AfterValidator(validators.validate_address)]
|
|
251
|
-
fsv_decimals: Annotated[int, Field(ge=0,
|
|
146
|
+
fsv_decimals: Annotated[int, Field(ge=0, le=255)] # uint8
|
|
252
147
|
fsp_alpha: Decimal
|
|
253
148
|
fsp_beta: Decimal
|
|
254
149
|
fsv_calldata: Annotated[str, AfterValidator(validators.validate_hexstr)]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
258
164
|
collateral_asset: Annotated[str, AfterValidator(validators.validate_address)]
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
maintenance_margin_requirement: Annotated[Decimal, Field(gt=0)]
|
|
264
|
-
auction_bounty: Annotated[Decimal, Field(ge=0, le=1)]
|
|
265
|
-
tradeout_interval: Annotated[int, Field(ge=0)]
|
|
266
|
-
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
|
|
267
169
|
|
|
268
|
-
def __str__(self) -> str:
|
|
269
|
-
return self.id
|
|
270
170
|
|
|
171
|
+
class PredictionProductV1(AliasedModel):
|
|
172
|
+
base: BaseProduct
|
|
173
|
+
expiry_spec: ExpirySpecification
|
|
174
|
+
min_price: Decimal
|
|
175
|
+
max_price: Decimal
|
|
271
176
|
|
|
272
|
-
|
|
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
|
|
273
184
|
|
|
274
185
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
|
|
280
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
|
|
281
309
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|