pmwallets 0.4.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.4.0 → pmwallets-0.5.0}/PKG-INFO +2 -2
- {pmwallets-0.4.0 → pmwallets-0.5.0}/README.md +1 -1
- {pmwallets-0.4.0 → pmwallets-0.5.0}/README.zh.md +1 -1
- {pmwallets-0.4.0 → pmwallets-0.5.0}/pyproject.toml +1 -1
- {pmwallets-0.4.0 → pmwallets-0.5.0}/src/pmwallets/client.py +108 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/tests/test_client.py +33 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/uv.lock +1 -1
- {pmwallets-0.4.0 → pmwallets-0.5.0}/.github/workflows/ci.yml +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/.github/workflows/publish.yml +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/.gitignore +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/LICENSE +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/src/pmwallets/__init__.py +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/src/pmwallets/stream.py +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/src/pmwallets/types.py +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/src/pmwallets/webhook.py +0 -0
- {pmwallets-0.4.0 → pmwallets-0.5.0}/tests/test_real_socket.py +0 -0
- {pmwallets-0.4.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, your unlocked addresses (`my_addresses`), 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, your unlocked addresses (`my_addresses`), 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"
|
|
@@ -116,11 +116,53 @@ class Client(_Base):
|
|
|
116
116
|
def latency(self) -> dict[str, Any]:
|
|
117
117
|
return self.request("GET", "/v1/latency")
|
|
118
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
|
+
|
|
119
157
|
# ── addresses
|
|
120
158
|
def buy_reveal(self, entity_id: str, max_price_cents: int) -> dict[str, Any]:
|
|
121
159
|
"""Buy the address behind a handle. `max_price_cents` is a ceiling: the charge never exceeds it."""
|
|
122
160
|
return self.request("POST", "/v1/account/reveals", body={"entityId": entity_id, "maxPriceCents": max_price_cents})
|
|
123
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
|
+
|
|
124
166
|
def reveals(self) -> list[dict[str, Any]]:
|
|
125
167
|
return self.request("GET", "/v1/account/reveals")
|
|
126
168
|
|
|
@@ -164,6 +206,18 @@ class Client(_Base):
|
|
|
164
206
|
"""Charges the balance; the price is recomputed server-side."""
|
|
165
207
|
return self.request("POST", "/v1/account/exports", body={"entityIds": entity_ids, "from": from_, "to": to})
|
|
166
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
|
+
|
|
167
221
|
def exports(self) -> list[dict[str, Any]]:
|
|
168
222
|
return self.request("GET", "/v1/account/exports")
|
|
169
223
|
|
|
@@ -222,9 +276,51 @@ class AsyncClient(_Base):
|
|
|
222
276
|
async def latency(self) -> dict[str, Any]:
|
|
223
277
|
return await self.request("GET", "/v1/latency")
|
|
224
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
|
+
|
|
225
317
|
async def buy_reveal(self, entity_id: str, max_price_cents: int) -> dict[str, Any]:
|
|
226
318
|
return await self.request("POST", "/v1/account/reveals", body={"entityId": entity_id, "maxPriceCents": max_price_cents})
|
|
227
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
|
+
|
|
228
324
|
async def reveals(self) -> list[dict[str, Any]]:
|
|
229
325
|
return await self.request("GET", "/v1/account/reveals")
|
|
230
326
|
|
|
@@ -260,6 +356,18 @@ class AsyncClient(_Base):
|
|
|
260
356
|
async def create_export(self, entity_ids: list[str], from_: str, to: str) -> dict[str, Any]:
|
|
261
357
|
return await self.request("POST", "/v1/account/exports", body={"entityIds": entity_ids, "from": from_, "to": to})
|
|
262
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
|
+
|
|
263
371
|
async def exports(self) -> list[dict[str, Any]]:
|
|
264
372
|
return await self.request("GET", "/v1/account/exports")
|
|
265
373
|
|
|
@@ -52,6 +52,39 @@ async def test_async_my_addresses():
|
|
|
52
52
|
assert str(calls[0].url) == "https://api.example.com/v1/leaderboard?mine=1"
|
|
53
53
|
|
|
54
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
|
+
|
|
55
88
|
def test_non_2xx_is_pmw_error():
|
|
56
89
|
calls = []
|
|
57
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
|