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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pmwallets
3
- Version: 0.1.2
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: a consumer hosted in the UK or elsewhere in Europe receives fills soonest.
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: a consumer hosted in the UK or elsewhere in Europe receives fills soonest.
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.1.2"
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"
@@ -10,4 +10,4 @@ __all__ = [
10
10
  "FileStateStore", "FillMeta", "FillStream", "MemoryStateStore", "StreamState", "UpgradeRefused", "websockets_connector",
11
11
  "Fill", "FillCursor", "FillsPage", "verify_webhook",
12
12
  ]
13
- __version__ = "0.1.2"
13
+ __version__ = "0.2.0"
@@ -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 export_download(self, export_id: str) -> dict[str, Any]:
158
- """A presigned download URL, valid 15 minutes."""
159
- return self.request("GET", f"/v1/account/exports/{_q(export_id)}/download")
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 export_download(self, export_id: str) -> dict[str, Any]:
237
- return await self.request("GET", f"/v1/account/exports/{_q(export_id)}/download")
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