pmwallets 0.1.2__tar.gz → 0.2.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.1.2 → pmwallets-0.2.0}/PKG-INFO +15 -2
- {pmwallets-0.1.2 → pmwallets-0.2.0}/README.md +14 -1
- {pmwallets-0.1.2 → pmwallets-0.2.0}/README.zh.md +14 -1
- {pmwallets-0.1.2 → pmwallets-0.2.0}/pyproject.toml +1 -1
- {pmwallets-0.1.2 → pmwallets-0.2.0}/src/pmwallets/__init__.py +1 -1
- {pmwallets-0.1.2 → pmwallets-0.2.0}/src/pmwallets/client.py +37 -5
- {pmwallets-0.1.2 → pmwallets-0.2.0}/tests/test_client.py +35 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/.github/workflows/ci.yml +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/.github/workflows/publish.yml +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/.gitignore +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/LICENSE +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/src/pmwallets/stream.py +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/src/pmwallets/types.py +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/src/pmwallets/webhook.py +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.0}/tests/test_real_socket.py +0 -0
- {pmwallets-0.1.2 → pmwallets-0.2.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.2.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
|
|
@@ -69,7 +69,20 @@ asyncio.run(main())
|
|
|
69
69
|
|
|
70
70
|
One stream per account. An API-key connection takes priority over the pmwallets.com feed page (the page never takes the stream from it); between two API connections the newest wins, so run one consumer per account. `HTTPS_PROXY` is honoured.
|
|
71
71
|
|
|
72
|
-
PMWallets' servers are in the United Kingdom
|
|
72
|
+
PMWallets' servers are in the United Kingdom. A consumer that also trades on Polymarket should run from Ireland (AWS eu-west-1): Polymarket does not accept API orders from the UK, the US and several EU countries.
|
|
73
|
+
|
|
74
|
+
## Trade-history exports
|
|
75
|
+
|
|
76
|
+
A purchase is access to files: one zstd-compressed CSV per wallet per UTC day, downloadable at once and again later.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
order = client.create_export(["0x…"], "2026-09-01", "2026-09-30")
|
|
80
|
+
for f in client.export_files(order["id"])["files"]:
|
|
81
|
+
data = client.download_export_file(f["wallet"], f["day"]) # .csv.zst
|
|
82
|
+
# pandas: pd.read_csv(io.BytesIO(data), compression="zstd")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`export_file_url(wallet, day)` returns the short-lived link instead; the API key is never sent to the storage host.
|
|
73
86
|
|
|
74
87
|
## Resources
|
|
75
88
|
|
|
@@ -51,7 +51,20 @@ asyncio.run(main())
|
|
|
51
51
|
|
|
52
52
|
One stream per account. An API-key connection takes priority over the pmwallets.com feed page (the page never takes the stream from it); between two API connections the newest wins, so run one consumer per account. `HTTPS_PROXY` is honoured.
|
|
53
53
|
|
|
54
|
-
PMWallets' servers are in the United Kingdom
|
|
54
|
+
PMWallets' servers are in the United Kingdom. A consumer that also trades on Polymarket should run from Ireland (AWS eu-west-1): Polymarket does not accept API orders from the UK, the US and several EU countries.
|
|
55
|
+
|
|
56
|
+
## Trade-history exports
|
|
57
|
+
|
|
58
|
+
A purchase is access to files: one zstd-compressed CSV per wallet per UTC day, downloadable at once and again later.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
order = client.create_export(["0x…"], "2026-09-01", "2026-09-30")
|
|
62
|
+
for f in client.export_files(order["id"])["files"]:
|
|
63
|
+
data = client.download_export_file(f["wallet"], f["day"]) # .csv.zst
|
|
64
|
+
# pandas: pd.read_csv(io.BytesIO(data), compression="zstd")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`export_file_url(wallet, day)` returns the short-lived link instead; the API key is never sent to the storage host.
|
|
55
68
|
|
|
56
69
|
## Resources
|
|
57
70
|
|
|
@@ -26,7 +26,20 @@ pip install pmwallets # Python ≥ 3.10
|
|
|
26
26
|
|
|
27
27
|
每个账户只有一条推送流。API key 建立的连接优先于 pmwallets.com 的推送页;两条 API 连接之间仍是后连的顶掉先连的。支持 `HTTPS_PROXY`。
|
|
28
28
|
|
|
29
|
-
PMWallets
|
|
29
|
+
PMWallets 的服务器在英国。若还要在 Polymarket 下单,请部署在爱尔兰(AWS eu-west-1):Polymarket 不接受来自英国、美国及部分欧盟国家的 API 订单。
|
|
30
|
+
|
|
31
|
+
## 历史成交导出
|
|
32
|
+
|
|
33
|
+
购买得到的是文件的访问权:每个钱包每个 UTC 日一个 zstd 压缩的 CSV,付款后立即可下载,之后也能再下。
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
order = client.create_export(["0x…"], "2026-09-01", "2026-09-30")
|
|
37
|
+
for f in client.export_files(order["id"])["files"]:
|
|
38
|
+
data = client.download_export_file(f["wallet"], f["day"]) # .csv.zst
|
|
39
|
+
# pandas: pd.read_csv(io.BytesIO(data), compression="zstd")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`export_file_url(wallet, day)` 只返回短时效链接;API key 不会发给存储服务。
|
|
30
43
|
|
|
31
44
|
## 相关链接
|
|
32
45
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pmwallets"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.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"
|
|
@@ -63,6 +63,15 @@ class _Base:
|
|
|
63
63
|
raise PmwError(res.status_code, body)
|
|
64
64
|
return body if text else None
|
|
65
65
|
|
|
66
|
+
@classmethod
|
|
67
|
+
def _location(cls, res: httpx.Response) -> str:
|
|
68
|
+
"""The redirect target of a purchased-file request; anything else is raised as PmwError."""
|
|
69
|
+
loc = res.headers.get("location")
|
|
70
|
+
if 300 <= res.status_code < 400 and loc:
|
|
71
|
+
return loc
|
|
72
|
+
cls._parse(res)
|
|
73
|
+
raise PmwError(res.status_code, res.text, "expected a redirect to the file")
|
|
74
|
+
|
|
66
75
|
@staticmethod
|
|
67
76
|
def _fills_query(since_block: int, since_log_index: int, limit: int) -> dict[str, Any]:
|
|
68
77
|
return {"sinceBlock": since_block, "sinceLogIndex": since_log_index, "limit": limit}
|
|
@@ -154,9 +163,22 @@ class Client(_Base):
|
|
|
154
163
|
def get_export(self, export_id: str) -> dict[str, Any]:
|
|
155
164
|
return self.request("GET", f"/v1/account/exports/{_q(export_id)}")
|
|
156
165
|
|
|
157
|
-
def
|
|
158
|
-
"""
|
|
159
|
-
return self.request("GET", f"/v1/account/exports/{_q(export_id)}/
|
|
166
|
+
def export_files(self, export_id: str) -> dict[str, Any]:
|
|
167
|
+
"""The daily files an export grants: one per wallet per UTC day."""
|
|
168
|
+
return self.request("GET", f"/v1/account/exports/{_q(export_id)}/files")
|
|
169
|
+
|
|
170
|
+
def export_file_url(self, wallet: str, day: str) -> str:
|
|
171
|
+
"""A short-lived (5 minute) link to one purchased daily file. The redirect is read, not followed,
|
|
172
|
+
so the API key is never sent to the storage host."""
|
|
173
|
+
res = self._http.request("GET", self.base_url + f"/v1/account/data/{_q(wallet)}/{_q(day)}", headers=self._headers, follow_redirects=False)
|
|
174
|
+
return self._location(res)
|
|
175
|
+
|
|
176
|
+
def download_export_file(self, wallet: str, day: str) -> bytes:
|
|
177
|
+
"""One purchased daily file: zstd-compressed CSV bytes (.csv.zst)."""
|
|
178
|
+
res = self._http.get(self.export_file_url(wallet, day), timeout=max(self.timeout, 60.0))
|
|
179
|
+
if res.status_code != 200:
|
|
180
|
+
raise PmwError(res.status_code, res.text)
|
|
181
|
+
return res.content
|
|
160
182
|
|
|
161
183
|
|
|
162
184
|
class AsyncClient(_Base):
|
|
@@ -233,5 +255,15 @@ class AsyncClient(_Base):
|
|
|
233
255
|
async def get_export(self, export_id: str) -> dict[str, Any]:
|
|
234
256
|
return await self.request("GET", f"/v1/account/exports/{_q(export_id)}")
|
|
235
257
|
|
|
236
|
-
async def
|
|
237
|
-
return await self.request("GET", f"/v1/account/exports/{_q(export_id)}/
|
|
258
|
+
async def export_files(self, export_id: str) -> dict[str, Any]:
|
|
259
|
+
return await self.request("GET", f"/v1/account/exports/{_q(export_id)}/files")
|
|
260
|
+
|
|
261
|
+
async def export_file_url(self, wallet: str, day: str) -> str:
|
|
262
|
+
res = await self._http.request("GET", self.base_url + f"/v1/account/data/{_q(wallet)}/{_q(day)}", headers=self._headers, follow_redirects=False)
|
|
263
|
+
return self._location(res)
|
|
264
|
+
|
|
265
|
+
async def download_export_file(self, wallet: str, day: str) -> bytes:
|
|
266
|
+
res = await self._http.get(await self.export_file_url(wallet, day), timeout=max(self.timeout, 60.0))
|
|
267
|
+
if res.status_code != 200:
|
|
268
|
+
raise PmwError(res.status_code, res.text)
|
|
269
|
+
return res.content
|
|
@@ -56,3 +56,38 @@ def test_verify_webhook():
|
|
|
56
56
|
assert not verify_webhook(body, sig, "other")
|
|
57
57
|
assert not verify_webhook(body, "zz", "sec")
|
|
58
58
|
assert not verify_webhook(body, None, "sec")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
W = "0x" + "ab" * 20
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _file_transport(calls):
|
|
65
|
+
def handler(req: httpx.Request):
|
|
66
|
+
calls.append(req)
|
|
67
|
+
if req.url.host == "api.example.com":
|
|
68
|
+
return httpx.Response(302, headers={"location": "https://r2.example/signed"})
|
|
69
|
+
return httpx.Response(200, content=b"\x01\x02\x03")
|
|
70
|
+
return httpx.MockTransport(handler)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_download_reads_the_redirect_and_never_sends_the_key_to_storage():
|
|
74
|
+
calls = []
|
|
75
|
+
c = Client("pmw_a_b", "https://api.example.com", http=httpx.Client(transport=_file_transport(calls)))
|
|
76
|
+
assert c.download_export_file(W, "2026-09-25") == b"\x01\x02\x03"
|
|
77
|
+
assert str(calls[0].url) == f"https://api.example.com/v1/account/data/{W}/2026-09-25"
|
|
78
|
+
assert str(calls[1].url) == "https://r2.example/signed"
|
|
79
|
+
assert "x-api-key" not in calls[1].headers
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def test_async_download_reads_the_redirect():
|
|
83
|
+
calls = []
|
|
84
|
+
c = AsyncClient("pmw_a_b", "https://api.example.com", http=httpx.AsyncClient(transport=_file_transport(calls)))
|
|
85
|
+
assert await c.download_export_file(W, "2026-09-25") == b"\x01\x02\x03"
|
|
86
|
+
assert "x-api-key" not in calls[1].headers
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_no_purchased_file_is_pmw_error():
|
|
90
|
+
c = Client("k", http=httpx.Client(transport=transport([{"statusCode": 404, "message": "no purchased file for this wallet and day"}], 404)))
|
|
91
|
+
with pytest.raises(PmwError) as e:
|
|
92
|
+
c.export_file_url(W, "2026-09-25")
|
|
93
|
+
assert e.value.status == 404
|
|
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
|