afp-sdk 0.5.4__py3-none-any.whl → 0.6.1__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 +236 -177
- afp/types.py +169 -0
- afp/validators.py +218 -8
- {afp_sdk-0.5.4.dist-info → afp_sdk-0.6.1.dist-info}/METADATA +76 -11
- afp_sdk-0.6.1.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.1.dist-info}/WHEEL +0 -0
- {afp_sdk-0.5.4.dist-info → afp_sdk-0.6.1.dist-info}/licenses/LICENSE +0 -0
afp/schemas.py
CHANGED
|
@@ -1,128 +1,58 @@
|
|
|
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
|
-
|
|
1
|
+
"""AFP data structures."""
|
|
84
2
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class ExchangeProductListingSubmission(Model):
|
|
89
|
-
id: Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from typing import Annotated, Any, ClassVar, Literal, Self
|
|
90
6
|
|
|
7
|
+
from pydantic import AfterValidator, BeforeValidator, Field, model_validator
|
|
91
8
|
|
|
92
|
-
|
|
93
|
-
|
|
9
|
+
from . import constants, validators
|
|
10
|
+
from .constants import schema_cids
|
|
11
|
+
from .enums import ListingState, OrderSide, OrderState, OrderType, TradeState
|
|
12
|
+
from .types import (
|
|
13
|
+
CID,
|
|
14
|
+
URL,
|
|
15
|
+
AliasedModel,
|
|
16
|
+
ISODate,
|
|
17
|
+
ISODateTime,
|
|
18
|
+
Model,
|
|
19
|
+
PinnedModel,
|
|
20
|
+
Timestamp,
|
|
21
|
+
)
|
|
94
22
|
|
|
95
23
|
|
|
96
24
|
# Trading API
|
|
97
25
|
|
|
98
26
|
|
|
99
|
-
class ExchangeProduct(
|
|
27
|
+
class ExchangeProduct(AliasedModel):
|
|
100
28
|
id: str
|
|
101
|
-
symbol: str
|
|
29
|
+
symbol: Annotated[str, AfterValidator(validators.validate_all_caps)]
|
|
102
30
|
tick_size: int
|
|
103
31
|
collateral_asset: str
|
|
104
32
|
listing_state: ListingState
|
|
33
|
+
min_price: Decimal
|
|
34
|
+
max_price: Decimal
|
|
105
35
|
|
|
106
36
|
def __str__(self) -> str:
|
|
107
37
|
return self.id
|
|
108
38
|
|
|
109
39
|
|
|
110
|
-
class
|
|
111
|
-
pass
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class IntentData(Model):
|
|
40
|
+
class IntentData(AliasedModel):
|
|
115
41
|
trading_protocol_id: str
|
|
116
42
|
product_id: str
|
|
117
|
-
limit_price:
|
|
43
|
+
limit_price: Decimal
|
|
118
44
|
quantity: Annotated[int, Field(gt=0)]
|
|
119
|
-
max_trading_fee_rate: Annotated[
|
|
45
|
+
max_trading_fee_rate: Annotated[
|
|
46
|
+
Decimal,
|
|
47
|
+
Field(le=Decimal((2**32 - 1) / constants.FEE_RATE_MULTIPLIER)), # uint32
|
|
48
|
+
]
|
|
120
49
|
side: OrderSide
|
|
121
50
|
good_until_time: Timestamp
|
|
122
51
|
nonce: int
|
|
52
|
+
referral: Annotated[str, AfterValidator(validators.validate_address)]
|
|
123
53
|
|
|
124
54
|
|
|
125
|
-
class Intent(
|
|
55
|
+
class Intent(AliasedModel):
|
|
126
56
|
hash: str
|
|
127
57
|
margin_account_id: str
|
|
128
58
|
intent_account_id: str
|
|
@@ -130,7 +60,7 @@ class Intent(Model):
|
|
|
130
60
|
data: IntentData
|
|
131
61
|
|
|
132
62
|
|
|
133
|
-
class Order(
|
|
63
|
+
class Order(AliasedModel):
|
|
134
64
|
id: str
|
|
135
65
|
type: OrderType
|
|
136
66
|
timestamp: Timestamp
|
|
@@ -139,35 +69,14 @@ class Order(Model):
|
|
|
139
69
|
intent: Intent
|
|
140
70
|
|
|
141
71
|
|
|
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):
|
|
72
|
+
class OrderCancellationData(AliasedModel):
|
|
158
73
|
intent_hash: Annotated[str, AfterValidator(validators.validate_hexstr32)]
|
|
159
74
|
nonce: int
|
|
160
75
|
intent_account_id: str
|
|
161
76
|
signature: str
|
|
162
77
|
|
|
163
78
|
|
|
164
|
-
class
|
|
165
|
-
type: OrderType
|
|
166
|
-
intent: Intent | None = None
|
|
167
|
-
cancellation_data: OrderCancellationData | None = None
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
class Trade(Model):
|
|
79
|
+
class Trade(AliasedModel):
|
|
171
80
|
# Convert ID from int to str for backward compatibility
|
|
172
81
|
id: Annotated[str, BeforeValidator(str)]
|
|
173
82
|
product_id: str
|
|
@@ -178,7 +87,7 @@ class Trade(Model):
|
|
|
178
87
|
rejection_reason: str | None
|
|
179
88
|
|
|
180
89
|
|
|
181
|
-
class OrderFill(
|
|
90
|
+
class OrderFill(AliasedModel):
|
|
182
91
|
order: Order
|
|
183
92
|
trade: Trade
|
|
184
93
|
quantity: int
|
|
@@ -186,32 +95,18 @@ class OrderFill(Model):
|
|
|
186
95
|
trading_fee_rate: Decimal
|
|
187
96
|
|
|
188
97
|
|
|
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):
|
|
98
|
+
class MarketDepthItem(AliasedModel):
|
|
204
99
|
price: Decimal
|
|
205
100
|
quantity: int
|
|
206
101
|
|
|
207
102
|
|
|
208
|
-
class MarketDepthData(
|
|
103
|
+
class MarketDepthData(AliasedModel):
|
|
209
104
|
product_id: str
|
|
210
105
|
bids: list[MarketDepthItem]
|
|
211
106
|
asks: list[MarketDepthItem]
|
|
212
107
|
|
|
213
108
|
|
|
214
|
-
class OHLCVItem(
|
|
109
|
+
class OHLCVItem(AliasedModel):
|
|
215
110
|
timestamp: Timestamp
|
|
216
111
|
open: Decimal
|
|
217
112
|
high: Decimal
|
|
@@ -220,7 +115,7 @@ class OHLCVItem(Model):
|
|
|
220
115
|
volume: int
|
|
221
116
|
|
|
222
117
|
|
|
223
|
-
#
|
|
118
|
+
# Margin Account API
|
|
224
119
|
|
|
225
120
|
|
|
226
121
|
class Transaction(Model):
|
|
@@ -237,51 +132,215 @@ class Position(Model):
|
|
|
237
132
|
pnl: Decimal
|
|
238
133
|
|
|
239
134
|
|
|
240
|
-
#
|
|
135
|
+
# Product API
|
|
241
136
|
|
|
242
137
|
|
|
243
|
-
class
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
138
|
+
class ExpirySpecification(AliasedModel):
|
|
139
|
+
earliest_fsp_submission_time: Annotated[
|
|
140
|
+
ISODateTime, Field(alias="earliestFSPSubmissionTime")
|
|
141
|
+
]
|
|
142
|
+
tradeout_interval: Annotated[int, Field(ge=0)]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class OracleSpecification(AliasedModel):
|
|
250
146
|
oracle_address: Annotated[str, AfterValidator(validators.validate_address)]
|
|
251
|
-
fsv_decimals: Annotated[int, Field(ge=0,
|
|
147
|
+
fsv_decimals: Annotated[int, Field(ge=0, le=255)] # uint8
|
|
252
148
|
fsp_alpha: Decimal
|
|
253
149
|
fsp_beta: Decimal
|
|
254
150
|
fsv_calldata: Annotated[str, AfterValidator(validators.validate_hexstr)]
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ProductMetadata(AliasedModel):
|
|
154
|
+
builder: Annotated[str, AfterValidator(validators.validate_address)]
|
|
155
|
+
symbol: Annotated[
|
|
156
|
+
str,
|
|
157
|
+
Field(pattern=r"^[A-Z0-9]{1,16}$", min_length=1, max_length=16),
|
|
158
|
+
]
|
|
159
|
+
description: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class BaseProduct(AliasedModel):
|
|
163
|
+
metadata: ProductMetadata
|
|
164
|
+
oracle_spec: OracleSpecification
|
|
258
165
|
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
|
|
166
|
+
start_time: ISODateTime
|
|
167
|
+
point_value: Decimal
|
|
168
|
+
price_decimals: Annotated[int, Field(ge=0, le=255)] # uint8
|
|
169
|
+
extended_metadata: CID | None = None
|
|
267
170
|
|
|
268
|
-
def __str__(self) -> str:
|
|
269
|
-
return self.id
|
|
270
171
|
|
|
172
|
+
class PredictionProductV1(AliasedModel):
|
|
173
|
+
base: BaseProduct
|
|
174
|
+
expiry_spec: ExpirySpecification
|
|
175
|
+
min_price: Decimal
|
|
176
|
+
max_price: Decimal
|
|
271
177
|
|
|
272
|
-
|
|
178
|
+
@model_validator(mode="after")
|
|
179
|
+
def _cross_validate(self) -> Self:
|
|
180
|
+
validators.validate_price_limits(self.min_price, self.max_price)
|
|
181
|
+
validators.validate_time_limits(
|
|
182
|
+
self.base.start_time, self.expiry_spec.earliest_fsp_submission_time
|
|
183
|
+
)
|
|
184
|
+
return self
|
|
273
185
|
|
|
274
186
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
187
|
+
# Extended metadata schemas
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class ApiSpec(Model):
|
|
191
|
+
standard: Literal["JSONPath", "GraphQL"]
|
|
192
|
+
spec_variant: Literal["underlying-history", "product-fsv"] | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ApiSpecJSONPath(ApiSpec):
|
|
196
|
+
standard: Literal["JSONPath"] = "JSONPath" # type: ignore
|
|
197
|
+
url: URL
|
|
198
|
+
date_path: str
|
|
199
|
+
value_path: str
|
|
200
|
+
auth_param_location: Literal["query", "header", "none"] = "none"
|
|
201
|
+
auth_param_name: str | None = None
|
|
202
|
+
auth_param_prefix: str | None = None
|
|
203
|
+
continuation_token_param: str | None = None
|
|
204
|
+
continuation_token_path: str | None = None
|
|
205
|
+
date_format_custom: str | None = None
|
|
206
|
+
date_format_type: Literal["iso_8601", "unix_timestamp", "custom"] = "iso_8601"
|
|
207
|
+
headers: dict[str, str] | None = None
|
|
208
|
+
max_pages: Annotated[int | None, Field(ge=1)] = 10
|
|
209
|
+
timestamp_scale: Annotated[int | float, Field(ge=1)] = 1
|
|
210
|
+
timezone: Annotated[
|
|
211
|
+
str,
|
|
212
|
+
Field(pattern=r"^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z][A-Za-z0-9_+-]*)*$"),
|
|
213
|
+
] = "UTC"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class BaseCaseResolution(Model):
|
|
217
|
+
condition: Annotated[str, Field(min_length=1)]
|
|
218
|
+
fsp_resolution: Annotated[str, Field(min_length=1)]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class EdgeCase(Model):
|
|
222
|
+
condition: Annotated[str, Field(min_length=1)]
|
|
223
|
+
fsp_resolution: Annotated[str, Field(min_length=1)]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class OutcomeSpace(PinnedModel):
|
|
227
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_SPACE_V020
|
|
228
|
+
|
|
229
|
+
fsp_type: Literal["scalar", "binary", "ternary"]
|
|
230
|
+
description: Annotated[str, Field(min_length=1)]
|
|
231
|
+
base_case: BaseCaseResolution
|
|
232
|
+
edge_cases: Annotated[list[EdgeCase], Field(default_factory=list)]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class OutcomeSpaceScalar(OutcomeSpace):
|
|
236
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_SPACE_SCALAR_V020
|
|
237
|
+
|
|
238
|
+
fsp_type: Literal["scalar"] = "scalar" # type: ignore
|
|
239
|
+
units: Annotated[str, Field(min_length=1)]
|
|
240
|
+
source_name: Annotated[str, Field(min_length=1)]
|
|
241
|
+
source_uri: URL
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class OutcomeSpaceTimeSeries(OutcomeSpaceScalar):
|
|
245
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_SPACE_TIME_SERIES_V020
|
|
246
|
+
|
|
247
|
+
frequency: Literal[
|
|
248
|
+
"daily",
|
|
249
|
+
"weekly",
|
|
250
|
+
"fortnightly",
|
|
251
|
+
"semimonthly",
|
|
252
|
+
"monthly",
|
|
253
|
+
"quarterly",
|
|
254
|
+
"yearly",
|
|
255
|
+
]
|
|
256
|
+
history_api_spec: ApiSpecJSONPath | ApiSpec | None = None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TemporalObservation(Model):
|
|
260
|
+
reference_date: ISODate
|
|
261
|
+
release_date: ISODate
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class OutcomePoint(PinnedModel):
|
|
265
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_POINT_V020
|
|
266
|
+
|
|
267
|
+
fsp_type: Literal["scalar", "binary", "ternary"]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class OutcomePointTimeSeries(OutcomePoint):
|
|
271
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_POINT_TIME_SERIES_V020
|
|
272
|
+
|
|
273
|
+
fsp_type: Literal["scalar"] = "scalar" # type: ignore
|
|
274
|
+
observation: TemporalObservation
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class OutcomePointEvent(OutcomePoint):
|
|
278
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.OUTCOME_POINT_EVENT_V020
|
|
279
|
+
|
|
280
|
+
fsp_type: Literal["binary", "ternary"] # type: ignore
|
|
281
|
+
outcome: Annotated[str, Field(min_length=1)]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class OracleConfig(PinnedModel):
|
|
285
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.ORACLE_CONFIG_V020
|
|
286
|
+
|
|
287
|
+
description: Annotated[str, Field(min_length=1)]
|
|
288
|
+
project_url: URL | None = None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class OracleConfigPrototype1(OracleConfig):
|
|
292
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.ORACLE_CONFIG_PROTOTYPE1_V020
|
|
293
|
+
|
|
294
|
+
evaluation_api_spec: ApiSpecJSONPath | ApiSpec
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class OracleFallback(PinnedModel):
|
|
298
|
+
SCHEMA_CID: ClassVar[CID] = schema_cids.ORACLE_FALLBACK_V020
|
|
299
|
+
|
|
300
|
+
fallback_time: ISODateTime
|
|
301
|
+
fallback_fsp: Decimal
|
|
302
|
+
|
|
280
303
|
|
|
304
|
+
class PredictionProduct(Model):
|
|
305
|
+
product: PredictionProductV1
|
|
306
|
+
outcome_space: OutcomeSpaceTimeSeries | OutcomeSpaceScalar | OutcomeSpace
|
|
307
|
+
outcome_point: OutcomePointEvent | OutcomePointTimeSeries | OutcomePoint
|
|
308
|
+
oracle_config: OracleConfigPrototype1 | OracleConfig
|
|
309
|
+
oracle_fallback: OracleFallback
|
|
281
310
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
311
|
+
@model_validator(mode="after")
|
|
312
|
+
def _cross_validate(self) -> Self:
|
|
313
|
+
validators.validate_matching_fsp_types(
|
|
314
|
+
self.outcome_space.fsp_type, self.outcome_point.fsp_type
|
|
315
|
+
)
|
|
316
|
+
validators.validate_oracle_fallback_time(
|
|
317
|
+
self.oracle_fallback.fallback_time,
|
|
318
|
+
self.product.expiry_spec.earliest_fsp_submission_time,
|
|
319
|
+
)
|
|
320
|
+
validators.validate_oracle_fallback_fsp(
|
|
321
|
+
self.oracle_fallback.fallback_fsp,
|
|
322
|
+
self.product.min_price,
|
|
323
|
+
self.product.max_price,
|
|
324
|
+
)
|
|
325
|
+
validators.validate_outcome_space_template_variables(
|
|
326
|
+
[
|
|
327
|
+
self.outcome_space.base_case.condition,
|
|
328
|
+
self.outcome_space.base_case.fsp_resolution,
|
|
329
|
+
]
|
|
330
|
+
+ list(
|
|
331
|
+
chain.from_iterable(
|
|
332
|
+
[edge_case.condition, edge_case.fsp_resolution]
|
|
333
|
+
for edge_case in self.outcome_space.edge_cases
|
|
334
|
+
)
|
|
335
|
+
),
|
|
336
|
+
self.outcome_point.model_dump(),
|
|
337
|
+
)
|
|
338
|
+
if isinstance(self.outcome_space, OutcomeSpaceTimeSeries) and isinstance(
|
|
339
|
+
self.outcome_point, OutcomePointTimeSeries
|
|
340
|
+
):
|
|
341
|
+
validators.validate_symbol(
|
|
342
|
+
self.product.base.metadata.symbol,
|
|
343
|
+
self.outcome_space.frequency,
|
|
344
|
+
self.outcome_point.observation.release_date,
|
|
345
|
+
)
|
|
346
|
+
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
|