pmwallets 0.3.0__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pmwallets
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Official PMWallets SDK: Polymarket smart-money leaderboard, real-time wallet fills over WebSocket with gap replay, webhooks and exports.
5
5
  Project-URL: Homepage, https://pmwallets.com/docs
6
6
  Project-URL: Repository, https://github.com/polymarketwallets/pmwallets-python
@@ -65,7 +65,7 @@ asyncio.run(main())
65
65
 
66
66
  | | |
67
67
  |---|---|
68
- | `Client` / `AsyncClient` | leaderboard, entities, address unlocks, subscriptions, fills replay (`fills`, `fills_since`), trade-history exports |
68
+ | `Client` / `AsyncClient` | leaderboard, entities, address unlocks (balance or plan), your unlocked addresses (`my_addresses`), plan and balance, subscriptions, webhook status and deliveries, fills replay (`fills`, `fills_since`), trade-history exports |
69
69
  | `FillStream` | asyncio WebSocket with reconnect and keep-alive; detects missed frames by `session`/`seq` and replays them from the last fill delivered; de-duplicates by `eventId`; anchors behind the chain head on first start; persisted cursor |
70
70
  | `verify_webhook(raw_body, signature, secret)` | checks `x-pmw-signature` (HMAC-SHA256 of the raw body) |
71
71
 
@@ -47,7 +47,7 @@ asyncio.run(main())
47
47
 
48
48
  | | |
49
49
  |---|---|
50
- | `Client` / `AsyncClient` | leaderboard, entities, address unlocks, subscriptions, fills replay (`fills`, `fills_since`), trade-history exports |
50
+ | `Client` / `AsyncClient` | leaderboard, entities, address unlocks (balance or plan), your unlocked addresses (`my_addresses`), plan and balance, subscriptions, webhook status and deliveries, fills replay (`fills`, `fills_since`), trade-history exports |
51
51
  | `FillStream` | asyncio WebSocket with reconnect and keep-alive; detects missed frames by `session`/`seq` and replays them from the last fill delivered; de-duplicates by `eventId`; anchors behind the chain head on first start; persisted cursor |
52
52
  | `verify_webhook(raw_body, signature, secret)` | checks `x-pmw-signature` (HMAC-SHA256 of the raw body) |
53
53
 
@@ -20,7 +20,7 @@ pip install pmwallets # Python ≥ 3.10
20
20
 
21
21
  | | |
22
22
  |---|---|
23
- | `Client` / `AsyncClient` | 排行榜、实体、地址解锁、订阅、成交补发(`fills`、`fills_since`)、交易历史导出 |
23
+ | `Client` / `AsyncClient` | 排行榜、实体、地址解锁(余额或套餐额度)、我的地址(`my_addresses`)、套餐与余额、订阅、webhook 状态与投递记录、成交补发(`fills`、`fills_since`)、交易历史导出 |
24
24
  | `FillStream` | asyncio WebSocket,自动重连与保活;按 `session`/`seq` 发现漏帧并从最后送达的成交补发;按 `eventId` 去重;首次启动锚定在链头之后;游标持久化 |
25
25
  | `verify_webhook(raw_body, signature, secret)` | 校验 `x-pmw-signature`(原始 body 的 HMAC-SHA256) |
26
26
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pmwallets"
7
- version = "0.3.0"
7
+ version = "0.5.0"
8
8
  description = "Official PMWallets SDK: Polymarket smart-money leaderboard, real-time wallet fills over WebSocket with gap replay, webhooks and exports."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -102,6 +102,13 @@ class Client(_Base):
102
102
  def leaderboard(self, **query: Any) -> Leaderboard:
103
103
  return self.request("GET", "/v1/leaderboard", query)
104
104
 
105
+ def my_addresses(self, **query: Any) -> Leaderboard:
106
+ """Every address you have unlocked (bought, free, or with your plan) as board rows. Each row has
107
+ `unlockedBy` ('paid' / 'free' / 'plan', or None when a free or plan unlock lapsed and is masked again)
108
+ and `pausedReason` when your push subscription to it is paused; `outside` (first page only) lists
109
+ unlocked addresses with no fill in the window. Spends no board rows."""
110
+ return self.request("GET", "/v1/leaderboard", {**query, "mine": 1})
111
+
105
112
  def entity(self, entity_id: str, period: Optional[str] = None) -> dict[str, Any]:
106
113
  """One entity by handle or address. The full address comes back only for entities you own."""
107
114
  return self.request("GET", f"/v1/entities/{_q(entity_id)}", {"period": period})
@@ -109,11 +116,53 @@ class Client(_Base):
109
116
  def latency(self) -> dict[str, Any]:
110
117
  return self.request("GET", "/v1/latency")
111
118
 
119
+ def periods(self) -> list[dict[str, Any]]:
120
+ """The published statistics windows (e.g. 30d, 7d) and when each was computed."""
121
+ return self.request("GET", "/v1/periods")
122
+
123
+ # ── account
124
+ def plan(self) -> dict[str, Any]:
125
+ """Your monthly plan (Base / Pro / Max), its period dates and what is used of its allowances."""
126
+ return self.request("GET", "/v1/account/plan")
127
+
128
+ def plans(self) -> dict[str, Any]:
129
+ """The plans on sale: price and allowances of Base / Pro / Max."""
130
+ return self.request("GET", "/v1/plans")
131
+
132
+ def billing(self) -> dict[str, Any]:
133
+ """Balance and top-up options."""
134
+ return self.request("GET", "/v1/account/billing")
135
+
136
+ def ledger(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> dict[str, Any]:
137
+ """One page of the balance statement, newest first; pass the previous page's `next` as `cursor`."""
138
+ return self.request("GET", "/v1/account/ledger", {"limit": limit, "cursor": cursor})
139
+
140
+ # ── webhooks
141
+ def webhooks(self) -> list[dict[str, Any]]:
142
+ """Your webhook endpoint. With an API key the URL comes back as its origin only; adding or removing one needs the website."""
143
+ return self.request("GET", "/v1/account/webhooks")
144
+
145
+ def reactivate_webhook(self, webhook_id: str) -> dict[str, Any]:
146
+ """Re-enable an endpoint that was disabled after a day of failures, and resume the subscriptions it paused."""
147
+ return self.request("POST", f"/v1/account/webhooks/{_q(webhook_id)}/reactivate")
148
+
149
+ def deliveries(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> dict[str, Any]:
150
+ """One page of delivery attempts (webhook and WebSocket), newest first."""
151
+ return self.request("GET", "/v1/account/deliveries", {"limit": limit, "cursor": cursor})
152
+
153
+ def delivery_stats(self, hours: Optional[int] = None) -> dict[str, Any]:
154
+ """Delivery success and latency over the last `hours`."""
155
+ return self.request("GET", "/v1/account/delivery-stats", {"hours": hours})
156
+
112
157
  # ── addresses
113
158
  def buy_reveal(self, entity_id: str, max_price_cents: int) -> dict[str, Any]:
114
159
  """Buy the address behind a handle. `max_price_cents` is a ceiling: the charge never exceeds it."""
115
160
  return self.request("POST", "/v1/account/reveals", body={"entityId": entity_id, "maxPriceCents": max_price_cents})
116
161
 
162
+ def unlock_with_plan(self, entity_id: str) -> dict[str, Any]:
163
+ """Unlock with your plan's allowance instead of the balance: never charges (409 when the allowance is used up)."""
164
+ return self.request("POST", "/v1/account/reveals", body={"entityId": entity_id, "maxPriceCents": 0, "viaPlan": True})
165
+
117
166
  def reveals(self) -> list[dict[str, Any]]:
118
167
  return self.request("GET", "/v1/account/reveals")
119
168
 
@@ -157,6 +206,18 @@ class Client(_Base):
157
206
  """Charges the balance; the price is recomputed server-side."""
158
207
  return self.request("POST", "/v1/account/exports", body={"entityIds": entity_ids, "from": from_, "to": to})
159
208
 
209
+ def create_plan_export(self, entity_ids: list[str]) -> dict[str, Any]:
210
+ """From a Max plan's monthly allowance: the whole sellable window, nothing charged."""
211
+ return self.request("POST", "/v1/account/exports", body={"entityIds": entity_ids, "viaPlan": True})
212
+
213
+ def export_window(self) -> dict[str, Any]:
214
+ """The days that can be bought right now (a contiguous range ending at the latest published day)."""
215
+ return self.request("GET", "/v1/account/exports/window")
216
+
217
+ def export_rate_card(self) -> dict[str, Any]:
218
+ """The per-day prices exports are charged at."""
219
+ return self.request("GET", "/v1/exports/rate-card")
220
+
160
221
  def exports(self) -> list[dict[str, Any]]:
161
222
  return self.request("GET", "/v1/account/exports")
162
223
 
@@ -205,15 +266,61 @@ class AsyncClient(_Base):
205
266
  async def leaderboard(self, **query: Any) -> Leaderboard:
206
267
  return await self.request("GET", "/v1/leaderboard", query)
207
268
 
269
+ async def my_addresses(self, **query: Any) -> Leaderboard:
270
+ """See Client.my_addresses."""
271
+ return await self.request("GET", "/v1/leaderboard", {**query, "mine": 1})
272
+
208
273
  async def entity(self, entity_id: str, period: Optional[str] = None) -> dict[str, Any]:
209
274
  return await self.request("GET", f"/v1/entities/{_q(entity_id)}", {"period": period})
210
275
 
211
276
  async def latency(self) -> dict[str, Any]:
212
277
  return await self.request("GET", "/v1/latency")
213
278
 
279
+ async def periods(self) -> list[dict[str, Any]]:
280
+ """The published statistics windows (e.g. 30d, 7d) and when each was computed."""
281
+ return await self.request("GET", "/v1/periods")
282
+
283
+ # ── account
284
+ async def plan(self) -> dict[str, Any]:
285
+ """Your monthly plan (Base / Pro / Max), its period dates and what is used of its allowances."""
286
+ return await self.request("GET", "/v1/account/plan")
287
+
288
+ async def plans(self) -> dict[str, Any]:
289
+ """The plans on sale: price and allowances of Base / Pro / Max."""
290
+ return await self.request("GET", "/v1/plans")
291
+
292
+ async def billing(self) -> dict[str, Any]:
293
+ """Balance and top-up options."""
294
+ return await self.request("GET", "/v1/account/billing")
295
+
296
+ async def ledger(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> dict[str, Any]:
297
+ """One page of the balance statement, newest first; pass the previous page's `next` as `cursor`."""
298
+ return await self.request("GET", "/v1/account/ledger", {"limit": limit, "cursor": cursor})
299
+
300
+ # ── webhooks
301
+ async def webhooks(self) -> list[dict[str, Any]]:
302
+ """Your webhook endpoint. With an API key the URL comes back as its origin only; adding or removing one needs the website."""
303
+ return await self.request("GET", "/v1/account/webhooks")
304
+
305
+ async def reactivate_webhook(self, webhook_id: str) -> dict[str, Any]:
306
+ """Re-enable an endpoint that was disabled after a day of failures, and resume the subscriptions it paused."""
307
+ return await self.request("POST", f"/v1/account/webhooks/{_q(webhook_id)}/reactivate")
308
+
309
+ async def deliveries(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> dict[str, Any]:
310
+ """One page of delivery attempts (webhook and WebSocket), newest first."""
311
+ return await self.request("GET", "/v1/account/deliveries", {"limit": limit, "cursor": cursor})
312
+
313
+ async def delivery_stats(self, hours: Optional[int] = None) -> dict[str, Any]:
314
+ """Delivery success and latency over the last `hours`."""
315
+ return await self.request("GET", "/v1/account/delivery-stats", {"hours": hours})
316
+
214
317
  async def buy_reveal(self, entity_id: str, max_price_cents: int) -> dict[str, Any]:
215
318
  return await self.request("POST", "/v1/account/reveals", body={"entityId": entity_id, "maxPriceCents": max_price_cents})
216
319
 
320
+ async def unlock_with_plan(self, entity_id: str) -> dict[str, Any]:
321
+ """Unlock with your plan's allowance instead of the balance: never charges (409 when the allowance is used up)."""
322
+ return await self.request("POST", "/v1/account/reveals", body={"entityId": entity_id, "maxPriceCents": 0, "viaPlan": True})
323
+
217
324
  async def reveals(self) -> list[dict[str, Any]]:
218
325
  return await self.request("GET", "/v1/account/reveals")
219
326
 
@@ -249,6 +356,18 @@ class AsyncClient(_Base):
249
356
  async def create_export(self, entity_ids: list[str], from_: str, to: str) -> dict[str, Any]:
250
357
  return await self.request("POST", "/v1/account/exports", body={"entityIds": entity_ids, "from": from_, "to": to})
251
358
 
359
+ async def create_plan_export(self, entity_ids: list[str]) -> dict[str, Any]:
360
+ """From a Max plan's monthly allowance: the whole sellable window, nothing charged."""
361
+ return await self.request("POST", "/v1/account/exports", body={"entityIds": entity_ids, "viaPlan": True})
362
+
363
+ async def export_window(self) -> dict[str, Any]:
364
+ """The days that can be bought right now (a contiguous range ending at the latest published day)."""
365
+ return await self.request("GET", "/v1/account/exports/window")
366
+
367
+ async def export_rate_card(self) -> dict[str, Any]:
368
+ """The per-day prices exports are charged at."""
369
+ return await self.request("GET", "/v1/exports/rate-card")
370
+
252
371
  async def exports(self) -> list[dict[str, Any]]:
253
372
  return await self.request("GET", "/v1/account/exports")
254
373
 
@@ -38,6 +38,53 @@ async def test_walks_every_page():
38
38
  assert "sinceBlock=2&sinceLogIndex=0&limit=2" in str(calls[1].url)
39
39
 
40
40
 
41
+ def test_my_addresses_asks_the_board_for_mine():
42
+ calls = []
43
+ c = Client("k", "https://api.example.com", http=httpx.Client(transport=transport([{"periodId": "30d", "tracked": 2, "rows": []}], calls=calls)))
44
+ assert c.my_addresses(sort="realizedPnl", offset=15)["tracked"] == 2
45
+ assert str(calls[0].url) == "https://api.example.com/v1/leaderboard?sort=realizedPnl&offset=15&mine=1"
46
+
47
+
48
+ async def test_async_my_addresses():
49
+ calls = []
50
+ c = AsyncClient("k", "https://api.example.com", http=httpx.AsyncClient(transport=transport([{"tracked": 0, "rows": []}], calls=calls)))
51
+ await c.my_addresses()
52
+ assert str(calls[0].url) == "https://api.example.com/v1/leaderboard?mine=1"
53
+
54
+
55
+ CASES = [
56
+ (lambda c: c.periods(), "GET", "/v1/periods", None),
57
+ (lambda c: c.plan(), "GET", "/v1/account/plan", None),
58
+ (lambda c: c.plans(), "GET", "/v1/plans", None),
59
+ (lambda c: c.billing(), "GET", "/v1/account/billing", None),
60
+ (lambda c: c.ledger(limit=10, cursor="x|1"), "GET", "/v1/account/ledger?limit=10&cursor=x%7C1", None),
61
+ (lambda c: c.unlock_with_plan("H1"), "POST", "/v1/account/reveals", {"entityId": "H1", "maxPriceCents": 0, "viaPlan": True}),
62
+ (lambda c: c.create_plan_export(["H1"]), "POST", "/v1/account/exports", {"entityIds": ["H1"], "viaPlan": True}),
63
+ (lambda c: c.export_window(), "GET", "/v1/account/exports/window", None),
64
+ (lambda c: c.export_rate_card(), "GET", "/v1/exports/rate-card", None),
65
+ (lambda c: c.webhooks(), "GET", "/v1/account/webhooks", None),
66
+ (lambda c: c.reactivate_webhook("w 1"), "POST", "/v1/account/webhooks/w%201/reactivate", None),
67
+ (lambda c: c.deliveries(limit=5), "GET", "/v1/account/deliveries?limit=5", None),
68
+ (lambda c: c.delivery_stats(hours=24), "GET", "/v1/account/delivery-stats?hours=24", None),
69
+ ]
70
+
71
+
72
+ @pytest.mark.parametrize("call,method,path,body", CASES)
73
+ def test_account_calls_map_to_endpoints(call, method, path, body):
74
+ calls = []
75
+ call(Client("k", "https://api.example.com", http=httpx.Client(transport=transport([{}], calls=calls))))
76
+ assert f"{calls[0].method} {calls[0].url}" == f"{method} https://api.example.com{path}"
77
+ if body is not None:
78
+ assert json.loads(calls[0].content) == body
79
+
80
+
81
+ @pytest.mark.parametrize("call,method,path,body", CASES)
82
+ async def test_async_account_calls_map_to_endpoints(call, method, path, body):
83
+ calls = []
84
+ await call(AsyncClient("k", "https://api.example.com", http=httpx.AsyncClient(transport=transport([{}], calls=calls))))
85
+ assert f"{calls[0].method} {calls[0].url}" == f"{method} https://api.example.com{path}"
86
+
87
+
41
88
  def test_non_2xx_is_pmw_error():
42
89
  calls = []
43
90
  c = Client("k", http=httpx.Client(transport=transport([{"statusCode": 402, "message": "insufficient balance"}], 402, calls)))
@@ -134,7 +134,7 @@ wheels = [
134
134
 
135
135
  [[package]]
136
136
  name = "pmwallets"
137
- version = "0.2.0"
137
+ version = "0.5.0"
138
138
  source = { editable = "." }
139
139
  dependencies = [
140
140
  { name = "httpx" },
File without changes
File without changes