polymarket-apis 0.3.0__py3-none-any.whl → 0.3.9__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.

Potentially problematic release.


This version of polymarket-apis might be problematic. Click here for more details.

Files changed (32) hide show
  1. polymarket_apis/__init__.py +42 -0
  2. polymarket_apis/clients/__init__.py +23 -0
  3. polymarket_apis/clients/clob_client.py +224 -117
  4. polymarket_apis/clients/data_client.py +220 -67
  5. polymarket_apis/clients/gamma_client.py +589 -101
  6. polymarket_apis/clients/graphql_client.py +28 -11
  7. polymarket_apis/clients/web3_client.py +538 -131
  8. polymarket_apis/clients/websockets_client.py +24 -7
  9. polymarket_apis/types/__init__.py +167 -0
  10. polymarket_apis/types/clob_types.py +35 -14
  11. polymarket_apis/types/common.py +105 -35
  12. polymarket_apis/types/data_types.py +48 -3
  13. polymarket_apis/types/gamma_types.py +529 -257
  14. polymarket_apis/types/web3_types.py +45 -0
  15. polymarket_apis/types/websockets_types.py +92 -41
  16. polymarket_apis/utilities/config.py +1 -0
  17. polymarket_apis/utilities/constants.py +5 -4
  18. polymarket_apis/utilities/exceptions.py +9 -0
  19. polymarket_apis/utilities/order_builder/builder.py +38 -22
  20. polymarket_apis/utilities/order_builder/helpers.py +0 -1
  21. polymarket_apis/utilities/signing/hmac.py +5 -1
  22. polymarket_apis/utilities/signing/signer.py +2 -2
  23. polymarket_apis/utilities/web3/abis/Safe.json +1138 -0
  24. polymarket_apis/utilities/web3/abis/SafeProxyFactory.json +224 -0
  25. polymarket_apis/utilities/web3/abis/custom_contract_errors.py +1 -1
  26. polymarket_apis/utilities/web3/helpers.py +235 -0
  27. {polymarket_apis-0.3.0.dist-info → polymarket_apis-0.3.9.dist-info}/METADATA +48 -8
  28. polymarket_apis-0.3.9.dist-info/RECORD +44 -0
  29. polymarket_apis/utilities/schemas/activity-subgraph.graphql +0 -86
  30. polymarket_apis/utilities/schemas/open-interest.graphql +0 -30
  31. polymarket_apis-0.3.0.dist-info/RECORD +0 -43
  32. {polymarket_apis-0.3.0.dist-info → polymarket_apis-0.3.9.dist-info}/WHEEL +0 -0
@@ -61,6 +61,7 @@ def _process_market_event(event):
61
61
  print(e.errors())
62
62
  print(event.json)
63
63
 
64
+
64
65
  def _process_user_event(event):
65
66
  try:
66
67
  message = event.json
@@ -75,6 +76,7 @@ def _process_user_event(event):
75
76
  print(event.text)
76
77
  print(e.errors(), "\n")
77
78
 
79
+
78
80
  def _process_live_data_event(event):
79
81
  try:
80
82
  message = event.json
@@ -87,7 +89,12 @@ def _process_live_data_event(event):
87
89
  print(CommentEvent(**message), "\n")
88
90
  case "reaction_created" | "reaction_removed":
89
91
  print(ReactionEvent(**message), "\n")
90
- case "request_created" | "request_edited" | "request_canceled" | "request_expired":
92
+ case (
93
+ "request_created"
94
+ | "request_edited"
95
+ | "request_canceled"
96
+ | "request_expired"
97
+ ):
91
98
  print(RequestEvent(**message), "\n")
92
99
  case "quote_created" | "quote_edited" | "quote_canceled" | "quote_expired":
93
100
  print(QuoteEvent(**message), "\n")
@@ -117,13 +124,16 @@ def _process_live_data_event(event):
117
124
  print(e.errors(), "\n")
118
125
  print(event.text)
119
126
 
127
+
120
128
  class PolymarketWebsocketsClient:
121
129
  def __init__(self):
122
130
  self.url_market = "wss://ws-subscriptions-clob.polymarket.com/ws/market"
123
131
  self.url_user = "wss://ws-subscriptions-clob.polymarket.com/ws/user"
124
132
  self.url_live_data = "wss://ws-live-data.polymarket.com"
125
133
 
126
- def market_socket(self, token_ids: list[str], process_event: Callable = _process_market_event):
134
+ def market_socket(
135
+ self, token_ids: list[str], process_event: Callable = _process_market_event
136
+ ):
127
137
  """
128
138
  Connect to the market websocket and subscribe to market events for specific token IDs.
129
139
 
@@ -142,7 +152,9 @@ class PolymarketWebsocketsClient:
142
152
  elif event.name == "text":
143
153
  process_event(event)
144
154
 
145
- def user_socket(self, creds: ApiCreds, process_event: Callable = _process_user_event):
155
+ def user_socket(
156
+ self, creds: ApiCreds, process_event: Callable = _process_user_event
157
+ ):
146
158
  """
147
159
  Connect to the user websocket and subscribe to user events.
148
160
 
@@ -161,7 +173,12 @@ class PolymarketWebsocketsClient:
161
173
  elif event.name == "text":
162
174
  process_event(event)
163
175
 
164
- def live_data_socket(self, subscriptions: list[dict[str, Any]], process_event: Callable = _process_live_data_event, creds: Optional[ApiCreds] = None):
176
+ def live_data_socket(
177
+ self,
178
+ subscriptions: list[dict[str, Any]],
179
+ process_event: Callable = _process_live_data_event,
180
+ creds: Optional[ApiCreds] = None,
181
+ ):
165
182
  # info on how to subscribe found at https://github.com/Polymarket/real-time-data-client?tab=readme-ov-file#subscribe
166
183
  """
167
184
  Connect to the live data websocket and subscribe to specified events.
@@ -175,13 +192,13 @@ class PolymarketWebsocketsClient:
175
192
  websocket = WebSocket(self.url_live_data)
176
193
 
177
194
  needs_auth = any(sub.get("topic") == "clob_user" for sub in subscriptions)
178
- if needs_auth and creds is None:
179
- msg = "ApiCreds credentials are required for the clob_user topic subscriptions"
180
- raise AuthenticationRequiredError(msg)
181
195
 
182
196
  for event in persist(websocket):
183
197
  if event.name == "ready":
184
198
  if needs_auth:
199
+ if creds is None:
200
+ msg = "ApiCreds credentials are required for the clob_user topic subscriptions"
201
+ raise AuthenticationRequiredError(msg)
185
202
  subscriptions_with_creds = []
186
203
  for sub in subscriptions:
187
204
  if sub.get("topic") == "clob_user":
@@ -0,0 +1,167 @@
1
+ # python
2
+ """
3
+ Type definitions for Polymarket APIs.
4
+
5
+ This module contains all the Pydantic models and type definitions used across
6
+ the Polymarket APIs.
7
+ """
8
+
9
+ from .clob_types import (
10
+ ApiCreds,
11
+ AssetType,
12
+ BidAsk,
13
+ BookParams,
14
+ ClobMarket,
15
+ ContractConfig,
16
+ CreateOrderOptions,
17
+ DailyEarnedReward,
18
+ MarketOrderArgs,
19
+ MarketRewards,
20
+ Midpoint,
21
+ OpenOrder,
22
+ OrderArgs,
23
+ OrderBookSummary,
24
+ OrderCancelResponse,
25
+ OrderPostResponse,
26
+ OrderType,
27
+ PaginatedResponse,
28
+ PartialCreateOrderOptions,
29
+ PolygonTrade,
30
+ PostOrdersArgs,
31
+ Price,
32
+ PriceHistory,
33
+ RewardMarket,
34
+ Spread,
35
+ TickSize,
36
+ Token,
37
+ TokenBidAsk,
38
+ TokenBidAskDict,
39
+ TokenValue,
40
+ TokenValueDict,
41
+ )
42
+ from .common import (
43
+ EmptyString,
44
+ EthAddress,
45
+ FlexibleDatetime,
46
+ Keccak256,
47
+ TimeseriesPoint,
48
+ )
49
+ from .data_types import (
50
+ Activity,
51
+ Holder,
52
+ HolderResponse,
53
+ Position,
54
+ Trade,
55
+ User,
56
+ UserMetric,
57
+ UserRank,
58
+ ValueResponse,
59
+ )
60
+ from .gamma_types import (
61
+ ClobReward,
62
+ Event,
63
+ GammaMarket,
64
+ Pagination,
65
+ Series,
66
+ Tag,
67
+ )
68
+ from .websockets_types import (
69
+ ActivityOrderMatchEvent,
70
+ ActivityTradeEvent,
71
+ CommentEvent,
72
+ CryptoPriceSubscribeEvent,
73
+ CryptoPriceUpdateEvent,
74
+ ErrorEvent,
75
+ LastTradePriceEvent,
76
+ LiveDataLastTradePriceEvent,
77
+ LiveDataOrderBookSummaryEvent,
78
+ LiveDataOrderEvent,
79
+ LiveDataPriceChangeEvent,
80
+ LiveDataTickSizeChangeEvent,
81
+ LiveDataTradeEvent,
82
+ MarketStatusChangeEvent,
83
+ OrderBookSummaryEvent,
84
+ OrderEvent,
85
+ PriceChangeEvent,
86
+ QuoteEvent,
87
+ ReactionEvent,
88
+ RequestEvent,
89
+ TickSizeChangeEvent,
90
+ TradeEvent,
91
+ )
92
+
93
+ __all__ = [
94
+ "Activity",
95
+ "ActivityOrderMatchEvent",
96
+ "ActivityTradeEvent",
97
+ "ApiCreds",
98
+ "AssetType",
99
+ "BidAsk",
100
+ "BookParams",
101
+ "ClobMarket",
102
+ "ClobReward",
103
+ "CommentEvent",
104
+ "ContractConfig",
105
+ "CreateOrderOptions",
106
+ "CryptoPriceSubscribeEvent",
107
+ "CryptoPriceUpdateEvent",
108
+ "DailyEarnedReward",
109
+ "EmptyString",
110
+ "ErrorEvent",
111
+ "EthAddress",
112
+ "Event",
113
+ "FlexibleDatetime",
114
+ "GammaMarket",
115
+ "Holder",
116
+ "HolderResponse",
117
+ "Keccak256",
118
+ "LastTradePriceEvent",
119
+ "LiveDataLastTradePriceEvent",
120
+ "LiveDataOrderBookSummaryEvent",
121
+ "LiveDataOrderEvent",
122
+ "LiveDataPriceChangeEvent",
123
+ "LiveDataTickSizeChangeEvent",
124
+ "LiveDataTradeEvent",
125
+ "MarketOrderArgs",
126
+ "MarketRewards",
127
+ "MarketStatusChangeEvent",
128
+ "Midpoint",
129
+ "OpenOrder",
130
+ "OrderArgs",
131
+ "OrderBookSummary",
132
+ "OrderBookSummaryEvent",
133
+ "OrderCancelResponse",
134
+ "OrderEvent",
135
+ "OrderPostResponse",
136
+ "OrderType",
137
+ "PaginatedResponse",
138
+ "Pagination",
139
+ "PartialCreateOrderOptions",
140
+ "PolygonTrade",
141
+ "Position",
142
+ "PostOrdersArgs",
143
+ "Price",
144
+ "PriceChangeEvent",
145
+ "PriceHistory",
146
+ "QuoteEvent",
147
+ "ReactionEvent",
148
+ "RequestEvent",
149
+ "RewardMarket",
150
+ "Series",
151
+ "Spread",
152
+ "Tag",
153
+ "TickSize",
154
+ "TickSizeChangeEvent",
155
+ "TimeseriesPoint",
156
+ "Token",
157
+ "TokenBidAsk",
158
+ "TokenBidAskDict",
159
+ "TokenValue",
160
+ "TokenValueDict",
161
+ "Trade",
162
+ "TradeEvent",
163
+ "User",
164
+ "UserMetric",
165
+ "UserRank",
166
+ "ValueResponse",
167
+ ]
@@ -17,10 +17,11 @@ from pydantic import (
17
17
  )
18
18
 
19
19
  from ..types.common import EthAddress, Keccak256, TimeseriesPoint
20
- from ..utilities.constants import ZERO_ADDRESS
20
+ from ..utilities.constants import ADDRESS_ZERO
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
+
24
25
  class ApiCreds(BaseModel):
25
26
  key: str = Field(alias="apiKey")
26
27
  secret: str
@@ -122,11 +123,13 @@ class Rewards(BaseModel):
122
123
  rewards_min_size: int = Field(alias="min_size")
123
124
  rewards_max_spread: float = Field(alias="max_spread")
124
125
 
126
+
125
127
  class EarnedReward(BaseModel):
126
128
  asset_address: EthAddress
127
129
  earnings: float
128
130
  asset_rate: float
129
131
 
132
+
130
133
  class DailyEarnedReward(BaseModel):
131
134
  date: datetime
132
135
  asset_address: EthAddress
@@ -134,12 +137,12 @@ class DailyEarnedReward(BaseModel):
134
137
  earnings: float
135
138
  asset_rate: float
136
139
 
137
- class PolymarketRewardItem(BaseModel):
140
+
141
+ class RewardMarket(BaseModel):
138
142
  market_id: str
139
143
  condition_id: Keccak256
140
144
  question: str
141
145
  market_slug: str
142
- market_description: str
143
146
  event_slug: str
144
147
  image: str
145
148
  maker_address: EthAddress
@@ -165,6 +168,7 @@ class MarketRewards(BaseModel):
165
168
  rewards_min_size: int
166
169
  market_competitiveness: float
167
170
 
171
+
168
172
  class ClobMarket(BaseModel):
169
173
  # Core market information
170
174
  token_ids: list[Token] = Field(alias="tokens")
@@ -215,7 +219,9 @@ class ClobMarket(BaseModel):
215
219
 
216
220
  @field_validator("neg_risk_market_id", "neg_risk_request_id", mode="wrap")
217
221
  @classmethod
218
- def validate_neg_risk_fields(cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Optional[str]:
222
+ def validate_neg_risk_fields(
223
+ cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
224
+ ) -> str | None:
219
225
  try:
220
226
  return handler(value)
221
227
  except ValidationError as e:
@@ -227,12 +233,18 @@ class ClobMarket(BaseModel):
227
233
  return value
228
234
  if neg_risk and value == "":
229
235
  for _ in e.errors():
230
- msg = ("Poorly setup market: negative risk is True, but either neg_risk_market_id or neg_risk_request_id is missing. "
231
- f" Question: {info.data.get("question")}; Market slug: {info.data.get('market_slug')} \n")
236
+ msg = (
237
+ "Poorly setup market: negative risk is True, but either neg_risk_market_id or neg_risk_request_id is missing. "
238
+ f" Question: {info.data.get('question')}; Market slug: {info.data.get('market_slug')} \n"
239
+ )
232
240
  logger.warning(msg)
241
+ return None
242
+
233
243
  @field_validator("condition_id", "question_id", mode="wrap")
234
244
  @classmethod
235
- def validate_condition_fields(cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> str:
245
+ def validate_condition_fields(
246
+ cls, value: str, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
247
+ ) -> str:
236
248
  try:
237
249
  return handler(value)
238
250
  except ValueError:
@@ -241,6 +253,7 @@ class ClobMarket(BaseModel):
241
253
  return value
242
254
  raise
243
255
 
256
+
244
257
  class OpenOrder(BaseModel):
245
258
  order_id: Keccak256 = Field(alias="id")
246
259
  status: str
@@ -258,6 +271,7 @@ class OpenOrder(BaseModel):
258
271
  associate_trades: list[str]
259
272
  created_at: datetime
260
273
 
274
+
261
275
  class MakerOrder(BaseModel):
262
276
  token_id: str = Field(alias="asset_id")
263
277
  order_id: Keccak256
@@ -268,6 +282,7 @@ class MakerOrder(BaseModel):
268
282
  outcome: str
269
283
  fee_rate_bps: float
270
284
 
285
+
271
286
  class PolygonTrade(BaseModel):
272
287
  trade_id: str = Field(alias="id")
273
288
  taker_order_id: Keccak256
@@ -277,7 +292,7 @@ class PolygonTrade(BaseModel):
277
292
  size: float
278
293
  fee_rate_bps: float
279
294
  price: float
280
- status: str # change to literals MINED, CONFIRMED
295
+ status: str # change to literals MINED, CONFIRMED
281
296
  match_time: datetime
282
297
  last_update: datetime
283
298
  outcome: str
@@ -288,6 +303,7 @@ class PolygonTrade(BaseModel):
288
303
  maker_orders: list[MakerOrder]
289
304
  trader_side: Literal["TAKER", "MAKER"]
290
305
 
306
+
291
307
  class TradeParams(BaseModel):
292
308
  id: Optional[str] = None
293
309
  maker_address: Optional[str] = None
@@ -304,12 +320,13 @@ class OpenOrderParams(BaseModel):
304
320
 
305
321
 
306
322
  class DropNotificationParams(BaseModel):
307
- ids: Optional[list[str]]= None
323
+ ids: Optional[list[str]] = None
308
324
 
309
325
 
310
326
  class OrderSummary(BaseModel):
311
- price: Optional[float] = None
312
- size: Optional[float] = None
327
+ price: float
328
+ size: float
329
+
313
330
 
314
331
  class PriceLevel(OrderSummary):
315
332
  side: Literal["BUY", "SELL"]
@@ -375,6 +392,7 @@ class RoundConfig(BaseModel):
375
392
  size: int
376
393
  amount: int
377
394
 
395
+
378
396
  class OrderArgs(BaseModel):
379
397
  token_id: str
380
398
  """
@@ -411,7 +429,7 @@ class OrderArgs(BaseModel):
411
429
  Timestamp after which the order is expired.
412
430
  """
413
431
 
414
- taker: str = ZERO_ADDRESS
432
+ taker: str = ADDRESS_ZERO
415
433
  """
416
434
  Address of the order taker. The zero address is used to indicate a public order.
417
435
  """
@@ -449,19 +467,21 @@ class MarketOrderArgs(BaseModel):
449
467
  Nonce used for onchain cancellations.
450
468
  """
451
469
 
452
- taker: str = ZERO_ADDRESS
470
+ taker: str = ADDRESS_ZERO
453
471
  """
454
472
  Address of the order taker. The zero address is used to indicate a public order.
455
473
  """
456
474
 
457
475
  order_type: OrderType = OrderType.FOK
458
476
 
477
+
459
478
  class PostOrdersArgs(BaseModel):
460
479
  order: SignedOrder
461
480
  order_type: OrderType = OrderType.GTC
462
481
 
463
482
  model_config = ConfigDict(arbitrary_types_allowed=True)
464
483
 
484
+
465
485
  class ContractConfig(BaseModel):
466
486
  """Contract Configuration."""
467
487
 
@@ -486,9 +506,10 @@ class OrderPostResponse(BaseModel):
486
506
  order_id: Union[Keccak256, Literal[""]] = Field(alias="orderID")
487
507
  taking_amount: str = Field(alias="takingAmount")
488
508
  making_amount: str = Field(alias="makingAmount")
489
- status: str = Literal["live", "matched", "delayed"]
509
+ status: Literal["live", "matched", "delayed"]
490
510
  success: bool
491
511
 
512
+
492
513
  class OrderCancelResponse(BaseModel):
493
514
  not_canceled: Optional[dict[Keccak256, str]]
494
515
  canceled: Optional[list[Keccak256]]
@@ -1,51 +1,121 @@
1
1
  import re
2
- from datetime import datetime
3
- from typing import Annotated
2
+ from datetime import UTC, datetime
3
+ from typing import Annotated, Any
4
4
 
5
- from pydantic import AfterValidator, BaseModel, BeforeValidator, Field
5
+ from dateutil import parser
6
+ from hexbytes import HexBytes
7
+ from pydantic import AfterValidator, BaseModel, BeforeValidator, ConfigDict, Field
6
8
 
7
9
 
8
- def validate_keccak256(v: str) -> str:
10
+ def parse_flexible_datetime(v: str | datetime) -> datetime:
11
+ """Parse datetime from multiple formats using dateutil."""
12
+ if v in {"NOW*()", "NOW()"}:
13
+ return datetime.fromtimestamp(0, tz=UTC)
14
+
15
+ if isinstance(v, str):
16
+ parsed = parser.parse(v)
17
+ if not isinstance(parsed, datetime):
18
+ msg = f"Failed to parse '{v}' as datetime, got {type(parsed)}"
19
+ raise TypeError(msg)
20
+ return parsed
21
+ return v
22
+
23
+
24
+ def validate_keccak256(v: str | HexBytes | bytes) -> str:
25
+ """Validate and normalize Keccak256 hash format."""
26
+ # Convert HexBytes/bytes to string
27
+ if isinstance(v, HexBytes | bytes):
28
+ v = v.hex()
29
+
30
+ # Ensure string and add 0x prefix if missing
31
+ if not isinstance(v, str):
32
+ msg = f"Expected string or bytes, got {type(v)}"
33
+ raise TypeError(msg)
34
+
35
+ if not v.startswith("0x"):
36
+ v = "0x" + v
37
+
38
+ # Validate format: 0x followed by 64 hex characters
9
39
  if not re.match(r"^0x[a-fA-F0-9]{64}$", v):
10
- msg = "Invalid Keccak256 hash format"
40
+ msg = f"Invalid Keccak256 hash format: {v}"
11
41
  raise ValueError(msg)
42
+
12
43
  return v
13
44
 
14
- def parse_timestamp(v: str) -> datetime:
15
- if isinstance(v, datetime):
45
+
46
+ def validate_eth_address(v: str | HexBytes | bytes) -> str:
47
+ """Validate and normalize Ethereum address format."""
48
+ # Convert HexBytes/bytes to string
49
+ if isinstance(v, HexBytes | bytes):
50
+ v = v.hex()
51
+
52
+ # Ensure string and add 0x prefix if missing
53
+ if not isinstance(v, str):
54
+ msg = f"Expected string or bytes, got {type(v)}"
55
+ raise TypeError(msg)
56
+
57
+ if not v.startswith("0x"):
58
+ v = "0x" + v
59
+
60
+ # Validate format: 0x followed by 40 hex characters
61
+ if not re.match(r"^0x[a-fA-F0-9]{40}$", v, re.IGNORECASE):
62
+ msg = f"Invalid Ethereum address format: {v}"
63
+ raise ValueError(msg)
64
+
65
+ return v
66
+
67
+
68
+ def hexbytes_to_str(v: Any) -> str:
69
+ """Convert HexBytes to hex string with 0x prefix."""
70
+ if isinstance(v, HexBytes):
71
+ hex_str = v.hex()
72
+ return hex_str if hex_str.startswith("0x") else f"0x{hex_str}"
73
+ if isinstance(v, bytes):
74
+ return "0x" + v.hex()
75
+ if isinstance(v, str) and not v.startswith("0x"):
76
+ return f"0x{v}"
77
+ return v
78
+
79
+
80
+ def validate_keccak_or_padded(v: Any) -> str:
81
+ """
82
+ Validate Keccak256 or accept padded addresses (32 bytes with leading zeros).
83
+
84
+ Some log topics are padded addresses, not proper Keccak256 hashes.
85
+ """
86
+ # First convert HexBytes/bytes to string with 0x prefix
87
+ if isinstance(v, HexBytes | bytes):
88
+ v = v.hex()
89
+
90
+ # Ensure it's a string
91
+ if not isinstance(v, str):
92
+ msg = f"Expected string or bytes, got {type(v)}"
93
+ raise TypeError(msg)
94
+
95
+ # Add 0x prefix if missing
96
+ if not v.startswith("0x"):
97
+ v = "0x" + v
98
+
99
+ # Accept 66 character hex strings (0x + 64 hex chars)
100
+ if len(v) == 66 and all(c in "0123456789abcdefABCDEF" for c in v[2:]):
16
101
  return v
17
- # Normalize '+00' to '+0000' for timezone
18
- if v.endswith("+00"):
19
- v = v[:-3] + "+0000"
20
-
21
- # Pad fractional seconds to 6 digits if present
22
- if "." in v:
23
- dot_pos = v.find(".")
24
- tz_pos = v.find("+", dot_pos) # Find timezone start after '.'
25
- if tz_pos == -1:
26
- tz_pos = v.find("-", dot_pos)
27
-
28
- if tz_pos != -1:
29
- frac = v[dot_pos+1:tz_pos]
30
- if len(frac) < 6:
31
- frac = frac.ljust(6, "0")
32
- v = f"{v[:dot_pos+1]}{frac}{v[tz_pos:]}"
33
-
34
- # Try parsing with and without microseconds
35
- for fmt in ("%Y-%m-%d %H:%M:%S.%f%z", "%Y-%m-%d %H:%M:%S%z"):
36
- try:
37
- return datetime.strptime(v, fmt) # noqa: DTZ007
38
- except ValueError:
39
- continue
40
- msg = f"Time data '{v}' does not match expected formats."
102
+
103
+ msg = (
104
+ f"Invalid hash format: expected 66 characters (0x + 64 hex), got {len(v)}: {v}"
105
+ )
41
106
  raise ValueError(msg)
42
107
 
43
108
 
44
- TimestampWithTZ = Annotated[datetime, BeforeValidator(parse_timestamp)]
45
- EthAddress = Annotated[str, Field(pattern=r"^0x[A-Fa-f0-9]{40}$")]
109
+ FlexibleDatetime = Annotated[datetime, BeforeValidator(parse_flexible_datetime)]
110
+ EthAddress = Annotated[str, AfterValidator(validate_eth_address)]
46
111
  Keccak256 = Annotated[str, AfterValidator(validate_keccak256)]
112
+ HexString = Annotated[str, BeforeValidator(hexbytes_to_str)]
113
+ Keccak256OrPadded = Annotated[str, BeforeValidator(validate_keccak_or_padded)]
47
114
  EmptyString = Annotated[str, Field(pattern=r"^$", description="An empty string")]
48
115
 
116
+
49
117
  class TimeseriesPoint(BaseModel):
50
- value: float
51
- timestamp: datetime
118
+ model_config = ConfigDict(populate_by_name=True)
119
+
120
+ value: float = Field(alias="p")
121
+ timestamp: datetime = Field(alias="t")
@@ -1,11 +1,43 @@
1
1
  from datetime import UTC, datetime
2
- from typing import Literal
2
+ from typing import Literal, Optional
3
3
 
4
- from pydantic import BaseModel, Field, field_validator
4
+ from pydantic import BaseModel, Field, field_validator, model_validator
5
5
 
6
6
  from .common import EmptyString, EthAddress, Keccak256
7
7
 
8
8
 
9
+ class GQLPosition(BaseModel):
10
+ user: EthAddress
11
+ token_id: str
12
+ complementary_token_id: str
13
+ condition_id: Keccak256
14
+ outcome_index: int
15
+ balance: float
16
+
17
+ @model_validator(mode="before")
18
+ def _flatten(cls, values):
19
+ asset = values.get("asset")
20
+ if isinstance(asset, dict):
21
+ if "id" in asset:
22
+ values.setdefault("token_id", asset["id"])
23
+ if "complement" in asset:
24
+ values.setdefault("complementary_token_id", asset["complement"])
25
+ condition = asset.get("condition")
26
+ if isinstance(condition, dict) and "id" in condition:
27
+ values.setdefault("condition_id", condition["id"])
28
+ if "outcomeIndex" in asset:
29
+ values.setdefault("outcome_index", asset["outcomeIndex"])
30
+ values.pop("asset", None)
31
+ return values
32
+
33
+ @field_validator("balance", mode="before")
34
+ @classmethod
35
+ def _parse_balance(cls, value):
36
+ if isinstance(value, str):
37
+ value = int(value)
38
+ return value / 10**6
39
+
40
+
9
41
  class Position(BaseModel):
10
42
  # User identification
11
43
  proxy_wallet: EthAddress = Field(alias="proxyWallet")
@@ -44,7 +76,7 @@ class Position(BaseModel):
44
76
  @field_validator("end_date", mode="before")
45
77
  def handle_empty_end_date(cls, v):
46
78
  if v == "":
47
- return datetime(2099,12,31, tzinfo=UTC)
79
+ return datetime(2099, 12, 31, tzinfo=UTC)
48
80
  return v
49
81
 
50
82
 
@@ -145,6 +177,7 @@ class ValueResponse(BaseModel):
145
177
  # Value information
146
178
  value: float
147
179
 
180
+
148
181
  class User(BaseModel):
149
182
  proxy_wallet: EthAddress = Field(alias="proxyWallet")
150
183
  name: str
@@ -152,10 +185,22 @@ class User(BaseModel):
152
185
  profile_image: str = Field(alias="profileImage")
153
186
  profile_image_optimized: str = Field(alias="profileImageOptimized")
154
187
 
188
+
155
189
  class UserMetric(User):
156
190
  amount: float
157
191
  pseudonym: str
158
192
 
193
+
159
194
  class UserRank(User):
160
195
  amount: float
161
196
  rank: int
197
+
198
+
199
+ class MarketValue(BaseModel):
200
+ condition_id: Keccak256 = Field(alias="market")
201
+ value: float
202
+
203
+
204
+ class EventLiveVolume(BaseModel):
205
+ total: Optional[float]
206
+ markets: Optional[list[MarketValue]]