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.
- {pmwallets-0.3.0 → pmwallets-0.5.0}/PKG-INFO +2 -2
- {pmwallets-0.3.0 → pmwallets-0.5.0}/README.md +1 -1
- {pmwallets-0.3.0 → pmwallets-0.5.0}/README.zh.md +1 -1
- {pmwallets-0.3.0 → pmwallets-0.5.0}/pyproject.toml +1 -1
- {pmwallets-0.3.0 → pmwallets-0.5.0}/src/pmwallets/client.py +119 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/tests/test_client.py +47 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/uv.lock +1 -1
- {pmwallets-0.3.0 → pmwallets-0.5.0}/.github/workflows/ci.yml +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/.github/workflows/publish.yml +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/.gitignore +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/LICENSE +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/src/pmwallets/__init__.py +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/src/pmwallets/stream.py +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/src/pmwallets/types.py +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/src/pmwallets/webhook.py +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/tests/test_real_socket.py +0 -0
- {pmwallets-0.3.0 → pmwallets-0.5.0}/tests/test_stream.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pmwallets
|
|
3
|
-
Version: 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` |
|
|
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.
|
|
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)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|