haozhuma 0.1.0__py3-none-any.whl
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.
- haozhuma/__init__.py +22 -0
- haozhuma/client.py +463 -0
- haozhuma/exceptions.py +32 -0
- haozhuma/models.py +82 -0
- haozhuma/provider.py +58 -0
- haozhuma/token_cache.py +86 -0
- haozhuma-0.1.0.dist-info/METADATA +191 -0
- haozhuma-0.1.0.dist-info/RECORD +10 -0
- haozhuma-0.1.0.dist-info/WHEEL +5 -0
- haozhuma-0.1.0.dist-info/top_level.txt +1 -0
haozhuma/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .client import HaozhuClient, from_token, login
|
|
2
|
+
from .exceptions import HaozhuAPIError, HaozhuError, HaozhuResponseError, HaozhuTimeoutError
|
|
3
|
+
from .models import AccountSummary, MessageResult, PhoneResult
|
|
4
|
+
from .provider import SMSProvider
|
|
5
|
+
from .token_cache import TokenCache
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AccountSummary",
|
|
9
|
+
"HaozhuAPIError",
|
|
10
|
+
"HaozhuClient",
|
|
11
|
+
"HaozhuError",
|
|
12
|
+
"HaozhuResponseError",
|
|
13
|
+
"HaozhuTimeoutError",
|
|
14
|
+
"MessageResult",
|
|
15
|
+
"PhoneResult",
|
|
16
|
+
"SMSProvider",
|
|
17
|
+
"TokenCache",
|
|
18
|
+
"from_token",
|
|
19
|
+
"login",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
haozhuma/client.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable, Mapping
|
|
7
|
+
|
|
8
|
+
from curl_cffi import requests
|
|
9
|
+
|
|
10
|
+
from .exceptions import HaozhuAPIError, HaozhuResponseError, HaozhuTimeoutError
|
|
11
|
+
from .models import AccountSummary, MessageResult, PhoneResult
|
|
12
|
+
from .token_cache import TokenCache
|
|
13
|
+
|
|
14
|
+
SUCCESS_CODES = {"0", "200"}
|
|
15
|
+
DEFAULT_SERVER = "api.haozhuma.com"
|
|
16
|
+
DEFAULT_AUTHOR = "adminzfz"
|
|
17
|
+
DEFAULT_POLL_INTERVAL = 15
|
|
18
|
+
DEFAULT_WAIT_TIMEOUT = 180
|
|
19
|
+
CODE_PATTERN = re.compile(r"(?<!\d)(\d{4,8})(?!\d)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HaozhuClient:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
server: str = DEFAULT_SERVER,
|
|
26
|
+
username: str = "",
|
|
27
|
+
password: str = "",
|
|
28
|
+
token: str = "",
|
|
29
|
+
project_id: str = "",
|
|
30
|
+
author: str = DEFAULT_AUTHOR,
|
|
31
|
+
timeout: int = 10,
|
|
32
|
+
poll_interval: int = DEFAULT_POLL_INTERVAL,
|
|
33
|
+
wait_timeout: int = DEFAULT_WAIT_TIMEOUT,
|
|
34
|
+
impersonate: str = "chrome120",
|
|
35
|
+
session: Any | None = None,
|
|
36
|
+
):
|
|
37
|
+
self.base_url = self.normalize_server(server)
|
|
38
|
+
self.username = str(username or "").strip()
|
|
39
|
+
self.password = str(password or "").strip()
|
|
40
|
+
self.project_id = str(project_id or "").strip()
|
|
41
|
+
self.author = str(author or DEFAULT_AUTHOR).strip()
|
|
42
|
+
self.timeout = int(timeout)
|
|
43
|
+
self.poll_interval = int(poll_interval)
|
|
44
|
+
self.wait_timeout = int(wait_timeout)
|
|
45
|
+
self.impersonate = str(impersonate or "").strip() or None
|
|
46
|
+
self.session = session or requests.Session()
|
|
47
|
+
self._token = str(token or "").strip()
|
|
48
|
+
self.last_phone: PhoneResult | None = None
|
|
49
|
+
self.last_message: MessageResult | None = None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_env(cls, prefix: str = "HAOZHUMA_") -> "HaozhuClient":
|
|
53
|
+
def env(name: str, default: str = "") -> str:
|
|
54
|
+
return os.getenv(f"{prefix}{name}", default)
|
|
55
|
+
|
|
56
|
+
return cls(
|
|
57
|
+
server=env("SERVER", DEFAULT_SERVER),
|
|
58
|
+
username=env("USER"),
|
|
59
|
+
password=env("PASSWORD"),
|
|
60
|
+
token=env("TOKEN"),
|
|
61
|
+
project_id=env("SID"),
|
|
62
|
+
author=env("AUTHOR", DEFAULT_AUTHOR),
|
|
63
|
+
poll_interval=int(env("POLL_INTERVAL", str(DEFAULT_POLL_INTERVAL))),
|
|
64
|
+
wait_timeout=int(env("WAIT_TIMEOUT", str(DEFAULT_WAIT_TIMEOUT))),
|
|
65
|
+
impersonate=env("IMPERSONATE", "chrome120"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def normalize_server(server: str) -> str:
|
|
70
|
+
value = str(server or DEFAULT_SERVER).strip().rstrip("/")
|
|
71
|
+
if value.startswith(("http://", "https://")):
|
|
72
|
+
return value
|
|
73
|
+
return f"https://{value}"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def token(self) -> str:
|
|
77
|
+
return self._token
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def phone(self) -> str:
|
|
81
|
+
if not self.last_phone:
|
|
82
|
+
return ""
|
|
83
|
+
return self.last_phone.phone
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def phone_info(self) -> PhoneResult | None:
|
|
87
|
+
return self.last_phone
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def message(self) -> MessageResult | None:
|
|
91
|
+
return self.last_message
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def code(self) -> str:
|
|
95
|
+
if not self.last_message:
|
|
96
|
+
return ""
|
|
97
|
+
return self.last_message.sms_code
|
|
98
|
+
|
|
99
|
+
def _sid(self, sid: str = "") -> str:
|
|
100
|
+
value = str(sid or self.project_id or "").strip()
|
|
101
|
+
if not value:
|
|
102
|
+
raise ValueError("豪猪项目ID为空,请传入 sid 或初始化 project_id")
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
def _phone(self, phone: str = "") -> str:
|
|
106
|
+
value = str(phone or self.phone or "").strip()
|
|
107
|
+
if not value:
|
|
108
|
+
raise ValueError("当前没有手机号,请先调用 get_phone(),或显式传入 phone")
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
def _request_kwargs(self, params: Mapping[str, Any]) -> dict[str, Any]:
|
|
112
|
+
kwargs: dict[str, Any] = {
|
|
113
|
+
"params": dict(params),
|
|
114
|
+
"timeout": self.timeout,
|
|
115
|
+
}
|
|
116
|
+
if self.impersonate:
|
|
117
|
+
kwargs["impersonate"] = self.impersonate
|
|
118
|
+
return kwargs
|
|
119
|
+
|
|
120
|
+
def request(self, api: str, allow_wait: bool = False, **params: Any) -> dict[str, Any]:
|
|
121
|
+
query: dict[str, Any] = {"api": api}
|
|
122
|
+
for key, value in params.items():
|
|
123
|
+
if value in (None, ""):
|
|
124
|
+
continue
|
|
125
|
+
query[key] = value
|
|
126
|
+
|
|
127
|
+
response = self.session.get(f"{self.base_url}/sms/", **self._request_kwargs(query))
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
response.encoding = "utf-8"
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
data = response.json()
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
raise HaozhuResponseError(f"豪猪接口返回非 JSON: api={api}, text={response.text[:300]}") from exc
|
|
135
|
+
|
|
136
|
+
if not isinstance(data, dict):
|
|
137
|
+
raise HaozhuResponseError(f"豪猪接口返回结构异常: api={api}, payload={data!r}")
|
|
138
|
+
|
|
139
|
+
code = str(data.get("code", "")).strip()
|
|
140
|
+
msg = str(data.get("msg") or "")
|
|
141
|
+
if code in SUCCESS_CODES:
|
|
142
|
+
return data
|
|
143
|
+
if allow_wait and code == "-1" and "等待" in msg:
|
|
144
|
+
return data
|
|
145
|
+
raise HaozhuAPIError(api, data)
|
|
146
|
+
|
|
147
|
+
def login(self, username: str = "", password: str = "", force: bool = False) -> str:
|
|
148
|
+
if self._token and not force:
|
|
149
|
+
return self._token
|
|
150
|
+
|
|
151
|
+
user = str(username or self.username or "").strip()
|
|
152
|
+
passwd = str(password or self.password or "").strip()
|
|
153
|
+
if not user or not passwd:
|
|
154
|
+
raise ValueError("豪猪 API 用户名或密码为空")
|
|
155
|
+
|
|
156
|
+
data = self.request("login", **{"user": user, "pass": passwd})
|
|
157
|
+
token = str(data.get("token") or "").strip()
|
|
158
|
+
if not token:
|
|
159
|
+
raise HaozhuResponseError(f"豪猪登录未返回 token: {data}")
|
|
160
|
+
self._token = token
|
|
161
|
+
return token
|
|
162
|
+
|
|
163
|
+
def ensure_token(self) -> str:
|
|
164
|
+
if self._token:
|
|
165
|
+
return self._token
|
|
166
|
+
return self.login()
|
|
167
|
+
|
|
168
|
+
def get_summary(self) -> AccountSummary:
|
|
169
|
+
data = self.request("getSummary", token=self.ensure_token())
|
|
170
|
+
return AccountSummary.from_payload(data)
|
|
171
|
+
|
|
172
|
+
def get_phone_raw(
|
|
173
|
+
self,
|
|
174
|
+
sid: str = "",
|
|
175
|
+
phone: str = "",
|
|
176
|
+
isp: str | int = "",
|
|
177
|
+
province: str | int = "",
|
|
178
|
+
ascription: str | int = "",
|
|
179
|
+
paragraph: str = "",
|
|
180
|
+
exclude: str = "",
|
|
181
|
+
uid: str = "",
|
|
182
|
+
author: str = "",
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
return self.request(
|
|
185
|
+
"getPhone",
|
|
186
|
+
token=self.ensure_token(),
|
|
187
|
+
sid=self._sid(sid),
|
|
188
|
+
phone=str(phone or ""),
|
|
189
|
+
isp=isp,
|
|
190
|
+
Province=province,
|
|
191
|
+
ascription=ascription,
|
|
192
|
+
paragraph=paragraph,
|
|
193
|
+
exclude=exclude,
|
|
194
|
+
uid=uid,
|
|
195
|
+
author=author or self.author,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def get_phone_info(self, sid: str = "", **filters: Any) -> PhoneResult:
|
|
199
|
+
self.last_phone = PhoneResult.from_payload(self.get_phone_raw(sid=sid, **filters))
|
|
200
|
+
return self.last_phone
|
|
201
|
+
|
|
202
|
+
def get_phone(self, sid: str = "", **filters: Any) -> str:
|
|
203
|
+
return self.get_phone_info(sid=sid, **filters).phone
|
|
204
|
+
|
|
205
|
+
def occupy_phone_info(self, phone: str, sid: str = "", author: str = "") -> PhoneResult:
|
|
206
|
+
self.last_phone = PhoneResult.from_payload(
|
|
207
|
+
self.get_phone_raw(
|
|
208
|
+
sid=sid,
|
|
209
|
+
phone=str(phone),
|
|
210
|
+
author=author or self.author,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
return self.last_phone
|
|
214
|
+
|
|
215
|
+
def occupy_phone(self, phone: str, sid: str = "", author: str = "") -> str:
|
|
216
|
+
return self.occupy_phone_info(phone=phone, sid=sid, author=author).phone
|
|
217
|
+
|
|
218
|
+
def get_message_raw(self, phone: str = "", sid: str = "", allow_wait: bool = False) -> dict[str, Any]:
|
|
219
|
+
phone_value = self._phone(phone)
|
|
220
|
+
return self.request(
|
|
221
|
+
"getMessage",
|
|
222
|
+
allow_wait=allow_wait,
|
|
223
|
+
token=self.ensure_token(),
|
|
224
|
+
sid=self._sid(sid),
|
|
225
|
+
phone=phone_value,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def get_message(self, phone: str = "", sid: str = "", allow_wait: bool = False) -> MessageResult:
|
|
229
|
+
sid_value = self._sid(sid)
|
|
230
|
+
phone_value = self._phone(phone)
|
|
231
|
+
payload = self.get_message_raw(phone_value, sid=sid_value, allow_wait=allow_wait)
|
|
232
|
+
self.last_message = MessageResult.from_payload(
|
|
233
|
+
phone=phone_value,
|
|
234
|
+
sid=sid_value,
|
|
235
|
+
payload=payload,
|
|
236
|
+
sms_code=self.extract_code(payload),
|
|
237
|
+
)
|
|
238
|
+
return self.last_message
|
|
239
|
+
|
|
240
|
+
def release_phone(self, phone: str = "", sid: str = "") -> dict[str, Any]:
|
|
241
|
+
return self.request(
|
|
242
|
+
"cancelRecv",
|
|
243
|
+
token=self.ensure_token(),
|
|
244
|
+
sid=self._sid(sid),
|
|
245
|
+
phone=self._phone(phone),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def release_all(self) -> dict[str, Any]:
|
|
249
|
+
return self.request("cancelAllRecv", token=self.ensure_token())
|
|
250
|
+
|
|
251
|
+
def blacklist_phone(self, phone: str = "", sid: str = "") -> dict[str, Any]:
|
|
252
|
+
return self.request(
|
|
253
|
+
"addBlacklist",
|
|
254
|
+
token=self.ensure_token(),
|
|
255
|
+
sid=self._sid(sid),
|
|
256
|
+
phone=self._phone(phone),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def block_phone(self, phone: str = "", sid: str = "") -> dict[str, Any]:
|
|
260
|
+
return self.blacklist_phone(phone=phone, sid=sid)
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def extract_code(payload: Mapping[str, Any] | str, fallback_code: str = "") -> str:
|
|
264
|
+
if isinstance(payload, Mapping):
|
|
265
|
+
fallback_code = str(payload.get("yzm") or fallback_code or "")
|
|
266
|
+
sms_text = str(payload.get("sms") or "")
|
|
267
|
+
else:
|
|
268
|
+
sms_text = str(payload or "")
|
|
269
|
+
|
|
270
|
+
digits = CODE_PATTERN.findall(fallback_code)
|
|
271
|
+
if digits:
|
|
272
|
+
return digits[0]
|
|
273
|
+
matches = CODE_PATTERN.findall(sms_text)
|
|
274
|
+
if matches:
|
|
275
|
+
return matches[0]
|
|
276
|
+
return ""
|
|
277
|
+
|
|
278
|
+
def poll_message(
|
|
279
|
+
self,
|
|
280
|
+
phone: str = "",
|
|
281
|
+
sid: str = "",
|
|
282
|
+
timeout: int | None = None,
|
|
283
|
+
interval: int | None = None,
|
|
284
|
+
auto_blacklist: bool = False,
|
|
285
|
+
on_attempt: Callable[[int, MessageResult], None] | None = None,
|
|
286
|
+
) -> MessageResult:
|
|
287
|
+
sid_value = self._sid(sid)
|
|
288
|
+
phone_value = self._phone(phone)
|
|
289
|
+
wait_timeout = int(timeout or self.wait_timeout)
|
|
290
|
+
poll_interval = int(interval or self.poll_interval)
|
|
291
|
+
deadline = time.monotonic() + wait_timeout
|
|
292
|
+
last_payload: dict[str, Any] | None = None
|
|
293
|
+
attempt = 0
|
|
294
|
+
|
|
295
|
+
while time.monotonic() <= deadline:
|
|
296
|
+
attempt += 1
|
|
297
|
+
last_payload = self.get_message_raw(phone_value, sid=sid_value, allow_wait=True)
|
|
298
|
+
message = MessageResult.from_payload(
|
|
299
|
+
phone=phone_value,
|
|
300
|
+
sid=sid_value,
|
|
301
|
+
payload=last_payload,
|
|
302
|
+
sms_code=self.extract_code(last_payload),
|
|
303
|
+
)
|
|
304
|
+
self.last_message = message
|
|
305
|
+
if on_attempt:
|
|
306
|
+
on_attempt(attempt, message)
|
|
307
|
+
if message.sms_code:
|
|
308
|
+
return message
|
|
309
|
+
sleep_for = min(poll_interval, max(0, deadline - time.monotonic()))
|
|
310
|
+
if sleep_for <= 0:
|
|
311
|
+
break
|
|
312
|
+
time.sleep(sleep_for)
|
|
313
|
+
|
|
314
|
+
if auto_blacklist:
|
|
315
|
+
self.blacklist_phone(phone_value, sid=sid_value)
|
|
316
|
+
raise HaozhuTimeoutError(phone=phone_value, sid=sid_value, last_payload=last_payload)
|
|
317
|
+
|
|
318
|
+
def poll_code(
|
|
319
|
+
self,
|
|
320
|
+
phone: str = "",
|
|
321
|
+
sid: str = "",
|
|
322
|
+
timeout: int | None = None,
|
|
323
|
+
interval: int | None = None,
|
|
324
|
+
auto_blacklist: bool = False,
|
|
325
|
+
on_attempt: Callable[[int, MessageResult], None] | None = None,
|
|
326
|
+
) -> str:
|
|
327
|
+
return self.poll_message(
|
|
328
|
+
phone=phone,
|
|
329
|
+
sid=sid,
|
|
330
|
+
timeout=timeout,
|
|
331
|
+
interval=interval,
|
|
332
|
+
auto_blacklist=auto_blacklist,
|
|
333
|
+
on_attempt=on_attempt,
|
|
334
|
+
).sms_code
|
|
335
|
+
|
|
336
|
+
def next_code(
|
|
337
|
+
self,
|
|
338
|
+
sid: str = "",
|
|
339
|
+
timeout: int | None = None,
|
|
340
|
+
interval: int | None = None,
|
|
341
|
+
auto_release: bool = True,
|
|
342
|
+
auto_blacklist: bool = True,
|
|
343
|
+
on_attempt: Callable[[int, MessageResult], None] | None = None,
|
|
344
|
+
**filters: Any,
|
|
345
|
+
) -> tuple[str, str]:
|
|
346
|
+
phone = self.get_phone(sid=sid, **filters)
|
|
347
|
+
try:
|
|
348
|
+
code = self.poll_code(
|
|
349
|
+
phone=phone,
|
|
350
|
+
sid=sid,
|
|
351
|
+
timeout=timeout,
|
|
352
|
+
interval=interval,
|
|
353
|
+
auto_blacklist=False,
|
|
354
|
+
on_attempt=on_attempt,
|
|
355
|
+
)
|
|
356
|
+
except HaozhuTimeoutError:
|
|
357
|
+
if auto_blacklist:
|
|
358
|
+
self.blacklist(phone=phone, sid=sid)
|
|
359
|
+
raise
|
|
360
|
+
if auto_release:
|
|
361
|
+
self.release(phone=phone, sid=sid)
|
|
362
|
+
return phone, code
|
|
363
|
+
|
|
364
|
+
def release(self, phone: str = "", sid: str = "") -> dict[str, Any]:
|
|
365
|
+
return self.release_phone(phone=phone, sid=sid)
|
|
366
|
+
|
|
367
|
+
def blacklist(self, phone: str = "", sid: str = "") -> dict[str, Any]:
|
|
368
|
+
return self.blacklist_phone(phone=phone, sid=sid)
|
|
369
|
+
|
|
370
|
+
def block(self, phone: str = "", sid: str = "") -> dict[str, Any]:
|
|
371
|
+
return self.blacklist_phone(phone=phone, sid=sid)
|
|
372
|
+
|
|
373
|
+
def close(self) -> None:
|
|
374
|
+
close = getattr(self.session, "close", None)
|
|
375
|
+
if callable(close):
|
|
376
|
+
close()
|
|
377
|
+
|
|
378
|
+
def __enter__(self) -> "HaozhuClient":
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
def __exit__(self, _exc_type: Any, _exc: Any, _tb: Any) -> None:
|
|
382
|
+
self.close()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def login(
|
|
386
|
+
user: str,
|
|
387
|
+
password: str,
|
|
388
|
+
sid: str = "",
|
|
389
|
+
server: str = DEFAULT_SERVER,
|
|
390
|
+
author: str = DEFAULT_AUTHOR,
|
|
391
|
+
timeout: int = 10,
|
|
392
|
+
poll_interval: int = DEFAULT_POLL_INTERVAL,
|
|
393
|
+
wait_timeout: int = DEFAULT_WAIT_TIMEOUT,
|
|
394
|
+
impersonate: str = "chrome120",
|
|
395
|
+
session: Any | None = None,
|
|
396
|
+
use_cache: bool = True,
|
|
397
|
+
token_cache: TokenCache | None = None,
|
|
398
|
+
force: bool = False,
|
|
399
|
+
) -> HaozhuClient:
|
|
400
|
+
"""Create a token-bearing client for batch registration code.
|
|
401
|
+
|
|
402
|
+
By default this reads TokenCache first. If no token exists, it logs in once,
|
|
403
|
+
writes the token cache, and returns a reusable stateful client.
|
|
404
|
+
"""
|
|
405
|
+
cache = token_cache or TokenCache()
|
|
406
|
+
if use_cache and not force:
|
|
407
|
+
cached_token = cache.get(server=server, user=user, sid=sid)
|
|
408
|
+
if cached_token:
|
|
409
|
+
return HaozhuClient(
|
|
410
|
+
server=server,
|
|
411
|
+
username=user,
|
|
412
|
+
password=password,
|
|
413
|
+
token=cached_token,
|
|
414
|
+
project_id=sid,
|
|
415
|
+
author=author,
|
|
416
|
+
timeout=timeout,
|
|
417
|
+
poll_interval=poll_interval,
|
|
418
|
+
wait_timeout=wait_timeout,
|
|
419
|
+
impersonate=impersonate,
|
|
420
|
+
session=session,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
client = HaozhuClient(
|
|
424
|
+
server=server,
|
|
425
|
+
username=user,
|
|
426
|
+
password=password,
|
|
427
|
+
project_id=sid,
|
|
428
|
+
author=author,
|
|
429
|
+
timeout=timeout,
|
|
430
|
+
poll_interval=poll_interval,
|
|
431
|
+
wait_timeout=wait_timeout,
|
|
432
|
+
impersonate=impersonate,
|
|
433
|
+
session=session,
|
|
434
|
+
)
|
|
435
|
+
token = client.login(force=True)
|
|
436
|
+
if use_cache:
|
|
437
|
+
cache.set(server=server, user=user, sid=sid, token=token)
|
|
438
|
+
return client
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def from_token(
|
|
442
|
+
token: str,
|
|
443
|
+
sid: str = "",
|
|
444
|
+
server: str = DEFAULT_SERVER,
|
|
445
|
+
author: str = DEFAULT_AUTHOR,
|
|
446
|
+
timeout: int = 10,
|
|
447
|
+
poll_interval: int = DEFAULT_POLL_INTERVAL,
|
|
448
|
+
wait_timeout: int = DEFAULT_WAIT_TIMEOUT,
|
|
449
|
+
impersonate: str = "chrome120",
|
|
450
|
+
session: Any | None = None,
|
|
451
|
+
) -> HaozhuClient:
|
|
452
|
+
"""Create a client from an existing token without calling login."""
|
|
453
|
+
return HaozhuClient(
|
|
454
|
+
server=server,
|
|
455
|
+
token=token,
|
|
456
|
+
project_id=sid,
|
|
457
|
+
author=author,
|
|
458
|
+
timeout=timeout,
|
|
459
|
+
poll_interval=poll_interval,
|
|
460
|
+
wait_timeout=wait_timeout,
|
|
461
|
+
impersonate=impersonate,
|
|
462
|
+
session=session,
|
|
463
|
+
)
|
haozhuma/exceptions.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HaozhuError(Exception):
|
|
7
|
+
"""Base exception for haozhuma client errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HaozhuAPIError(HaozhuError):
|
|
11
|
+
"""Raised when Haozhuma returns a non-success response."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, api: str, payload: Mapping[str, Any]):
|
|
14
|
+
self.api = api
|
|
15
|
+
self.payload = dict(payload)
|
|
16
|
+
code = self.payload.get("code")
|
|
17
|
+
msg = self.payload.get("msg")
|
|
18
|
+
super().__init__(f"豪猪接口失败: api={api}, code={code}, msg={msg}, payload={self.payload}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HaozhuResponseError(HaozhuError):
|
|
22
|
+
"""Raised when the HTTP response is not valid JSON or has an invalid shape."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HaozhuTimeoutError(HaozhuError, TimeoutError):
|
|
26
|
+
"""Raised when polling SMS code times out."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, phone: str, sid: str, last_payload: Mapping[str, Any] | None = None):
|
|
29
|
+
self.phone = str(phone)
|
|
30
|
+
self.sid = str(sid)
|
|
31
|
+
self.last_payload = dict(last_payload or {})
|
|
32
|
+
super().__init__(f"等待验证码超时: phone={self.phone}, sid={self.sid}, last={self.last_payload}")
|
haozhuma/models.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class AccountSummary:
|
|
9
|
+
money: str = ""
|
|
10
|
+
num: int = 0
|
|
11
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "AccountSummary":
|
|
15
|
+
raw = dict(payload)
|
|
16
|
+
try:
|
|
17
|
+
num = int(raw.get("num") or 0)
|
|
18
|
+
except (TypeError, ValueError):
|
|
19
|
+
num = 0
|
|
20
|
+
return cls(
|
|
21
|
+
money=str(raw.get("money") or ""),
|
|
22
|
+
num=num,
|
|
23
|
+
raw=raw,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class PhoneResult:
|
|
29
|
+
phone: str
|
|
30
|
+
sid: str = ""
|
|
31
|
+
shop_name: str = ""
|
|
32
|
+
country_name: str = ""
|
|
33
|
+
country_code: str = ""
|
|
34
|
+
country_qu: str = ""
|
|
35
|
+
uid: str = ""
|
|
36
|
+
sp: str = ""
|
|
37
|
+
phone_gsd: str = ""
|
|
38
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "PhoneResult":
|
|
42
|
+
raw = dict(payload)
|
|
43
|
+
return cls(
|
|
44
|
+
phone=str(raw.get("phone") or ""),
|
|
45
|
+
sid=str(raw.get("sid") or ""),
|
|
46
|
+
shop_name=str(raw.get("shop_name") or ""),
|
|
47
|
+
country_name=str(raw.get("country_name") or ""),
|
|
48
|
+
country_code=str(raw.get("country_code") or ""),
|
|
49
|
+
country_qu=str(raw.get("country_qu") or ""),
|
|
50
|
+
uid=str(raw.get("uid") or ""),
|
|
51
|
+
sp=str(raw.get("sp") or ""),
|
|
52
|
+
phone_gsd=str(raw.get("phone_gsd") or ""),
|
|
53
|
+
raw=raw,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class MessageResult:
|
|
59
|
+
phone: str
|
|
60
|
+
sid: str
|
|
61
|
+
sms: str = ""
|
|
62
|
+
sms_code: str = ""
|
|
63
|
+
api_code: str = ""
|
|
64
|
+
msg: str = ""
|
|
65
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def ok(self) -> bool:
|
|
69
|
+
return self.api_code in {"0", "200"} and bool(self.sms_code)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_payload(cls, phone: str, sid: str, payload: Mapping[str, Any], sms_code: str = "") -> "MessageResult":
|
|
73
|
+
raw = dict(payload)
|
|
74
|
+
return cls(
|
|
75
|
+
phone=str(phone),
|
|
76
|
+
sid=str(sid),
|
|
77
|
+
sms=str(raw.get("sms") or ""),
|
|
78
|
+
sms_code=str(sms_code or raw.get("yzm") or ""),
|
|
79
|
+
api_code=str(raw.get("code", "")),
|
|
80
|
+
msg=str(raw.get("msg") or ""),
|
|
81
|
+
raw=raw,
|
|
82
|
+
)
|
haozhuma/provider.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from .models import MessageResult, PhoneResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class SMSProvider(Protocol):
|
|
10
|
+
@property
|
|
11
|
+
def token(self) -> str: ...
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def phone(self) -> str: ...
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def code(self) -> str: ...
|
|
18
|
+
|
|
19
|
+
def get_phone(self, sid: str = "", **filters: Any) -> str: ...
|
|
20
|
+
|
|
21
|
+
def get_phone_info(self, sid: str = "", **filters: Any) -> PhoneResult: ...
|
|
22
|
+
|
|
23
|
+
def poll_code(
|
|
24
|
+
self,
|
|
25
|
+
phone: str = "",
|
|
26
|
+
sid: str = "",
|
|
27
|
+
timeout: int | None = None,
|
|
28
|
+
interval: int | None = None,
|
|
29
|
+
auto_blacklist: bool = False,
|
|
30
|
+
on_attempt: Callable[[int, MessageResult], None] | None = None,
|
|
31
|
+
) -> str: ...
|
|
32
|
+
|
|
33
|
+
def poll_message(
|
|
34
|
+
self,
|
|
35
|
+
phone: str = "",
|
|
36
|
+
sid: str = "",
|
|
37
|
+
timeout: int | None = None,
|
|
38
|
+
interval: int | None = None,
|
|
39
|
+
auto_blacklist: bool = False,
|
|
40
|
+
on_attempt: Callable[[int, MessageResult], None] | None = None,
|
|
41
|
+
) -> MessageResult: ...
|
|
42
|
+
|
|
43
|
+
def next_code(
|
|
44
|
+
self,
|
|
45
|
+
sid: str = "",
|
|
46
|
+
timeout: int | None = None,
|
|
47
|
+
interval: int | None = None,
|
|
48
|
+
auto_release: bool = True,
|
|
49
|
+
auto_blacklist: bool = True,
|
|
50
|
+
on_attempt: Callable[[int, MessageResult], None] | None = None,
|
|
51
|
+
**filters: Any,
|
|
52
|
+
) -> tuple[str, str]: ...
|
|
53
|
+
|
|
54
|
+
def release(self, phone: str = "", sid: str = "") -> dict[str, Any]: ...
|
|
55
|
+
|
|
56
|
+
def blacklist(self, phone: str = "", sid: str = "") -> dict[str, Any]: ...
|
|
57
|
+
|
|
58
|
+
def close(self) -> None: ...
|
haozhuma/token_cache.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
TOKEN_CACHE_ENV = "HAOZHUMA_TOKEN_CACHE"
|
|
10
|
+
APP_DIR_NAME = "haozhuma"
|
|
11
|
+
TOKEN_CACHE_FILE_NAME = "token_cache.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenCache:
|
|
15
|
+
def __init__(self, path: str | Path | None = None):
|
|
16
|
+
self.path = Path(path).expanduser().resolve() if path else self.default_path()
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def default_path() -> Path:
|
|
20
|
+
override = os.getenv(TOKEN_CACHE_ENV)
|
|
21
|
+
if override:
|
|
22
|
+
return Path(override).expanduser().resolve()
|
|
23
|
+
|
|
24
|
+
appdata = os.getenv("APPDATA")
|
|
25
|
+
if appdata:
|
|
26
|
+
return Path(appdata) / APP_DIR_NAME / TOKEN_CACHE_FILE_NAME
|
|
27
|
+
|
|
28
|
+
return Path.home() / ".cache" / APP_DIR_NAME / TOKEN_CACHE_FILE_NAME
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def make_key(server: str, user: str, sid: str = "") -> str:
|
|
32
|
+
server_value = str(server or "").strip().rstrip("/").lower()
|
|
33
|
+
user_value = str(user or "").strip()
|
|
34
|
+
sid_value = str(sid or "").strip()
|
|
35
|
+
return "|".join([server_value, user_value, sid_value])
|
|
36
|
+
|
|
37
|
+
def load(self) -> dict[str, dict[str, Any]]:
|
|
38
|
+
if not self.path.exists():
|
|
39
|
+
return {}
|
|
40
|
+
try:
|
|
41
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
42
|
+
except json.JSONDecodeError:
|
|
43
|
+
return {}
|
|
44
|
+
if not isinstance(data, dict):
|
|
45
|
+
return {}
|
|
46
|
+
return {str(key): dict(value) for key, value in data.items() if isinstance(value, dict)}
|
|
47
|
+
|
|
48
|
+
def save(self, data: dict[str, dict[str, Any]]) -> Path:
|
|
49
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self.path.write_text(
|
|
51
|
+
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
)
|
|
54
|
+
return self.path
|
|
55
|
+
|
|
56
|
+
def get(self, server: str, user: str, sid: str = "") -> str:
|
|
57
|
+
key = self.make_key(server, user, sid)
|
|
58
|
+
entry = self.load().get(key) or {}
|
|
59
|
+
return str(entry.get("token") or "").strip()
|
|
60
|
+
|
|
61
|
+
def set(self, server: str, user: str, sid: str, token: str) -> Path:
|
|
62
|
+
key = self.make_key(server, user, sid)
|
|
63
|
+
data = self.load()
|
|
64
|
+
data[key] = {
|
|
65
|
+
"server": str(server or ""),
|
|
66
|
+
"user": str(user or ""),
|
|
67
|
+
"sid": str(sid or ""),
|
|
68
|
+
"token": str(token or ""),
|
|
69
|
+
"updated_at": int(time.time()),
|
|
70
|
+
}
|
|
71
|
+
return self.save(data)
|
|
72
|
+
|
|
73
|
+
def delete(self, server: str, user: str, sid: str = "") -> bool:
|
|
74
|
+
key = self.make_key(server, user, sid)
|
|
75
|
+
data = self.load()
|
|
76
|
+
if key not in data:
|
|
77
|
+
return False
|
|
78
|
+
data.pop(key, None)
|
|
79
|
+
self.save(data)
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def clear(self) -> bool:
|
|
83
|
+
if self.path.exists():
|
|
84
|
+
self.path.unlink()
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haozhuma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Haozhuma SMS receiving platform Python client
|
|
5
|
+
Author: laozig
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://api.haozhuma.com
|
|
8
|
+
Project-URL: Documentation, https://api.haozhuma.com
|
|
9
|
+
Keywords: haozhuma,sms,otp,verification-code
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: curl_cffi>=0.7.0
|
|
22
|
+
|
|
23
|
+
# haozhuma
|
|
24
|
+
|
|
25
|
+
`haozhuma` 是豪猪接码平台的 **Python 代码 SDK**,面向注册机、批量任务和接码流程封装。
|
|
26
|
+
|
|
27
|
+
当前版本已经移除了 CLI,只保留代码调用入口。
|
|
28
|
+
|
|
29
|
+
## 快速启动
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd haozhuma
|
|
33
|
+
python -m pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 环境依赖
|
|
37
|
+
|
|
38
|
+
| 依赖 | 最低版本 | 用途 |
|
|
39
|
+
|---|---:|---|
|
|
40
|
+
| `Python` | `3.9` | 运行 SDK |
|
|
41
|
+
| `curl_cffi` | `0.7.0` | HTTP 客户端 |
|
|
42
|
+
|
|
43
|
+
## 最短用法
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from haozhuma import login
|
|
47
|
+
|
|
48
|
+
with login("your_user", "your_password", sid="92162") as sms:
|
|
49
|
+
phone, code = sms.next_code()
|
|
50
|
+
print(phone, code)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
这段代码内部完成:
|
|
54
|
+
|
|
55
|
+
- 登录豪猪平台。
|
|
56
|
+
- 复用 `token`。
|
|
57
|
+
- 获取手机号。
|
|
58
|
+
- 轮询验证码。
|
|
59
|
+
- 成功后自动释放号码。
|
|
60
|
+
|
|
61
|
+
## 状态机用法
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from haozhuma import login
|
|
65
|
+
|
|
66
|
+
sms = login("your_user", "your_password", sid="92162")
|
|
67
|
+
|
|
68
|
+
phone = sms.get_phone()
|
|
69
|
+
code = sms.poll_code()
|
|
70
|
+
|
|
71
|
+
print(phone, code)
|
|
72
|
+
print(sms.phone, sms.code)
|
|
73
|
+
|
|
74
|
+
sms.release()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## `next_code()` 用法
|
|
78
|
+
|
|
79
|
+
`next_code()` 适合注册机主循环。
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from haozhuma import login
|
|
83
|
+
|
|
84
|
+
sms = login("your_user", "your_password", sid="92162")
|
|
85
|
+
|
|
86
|
+
for index in range(100):
|
|
87
|
+
try:
|
|
88
|
+
phone, code = sms.next_code()
|
|
89
|
+
print(index, phone, code)
|
|
90
|
+
except TimeoutError:
|
|
91
|
+
print(index, "timeout")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
默认行为:
|
|
95
|
+
|
|
96
|
+
| 参数 | 默认值 | 作用 |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `auto_release` | `True` | 取码成功后自动释放号码 |
|
|
99
|
+
| `auto_blacklist` | `True` | 超时后自动拉黑号码 |
|
|
100
|
+
|
|
101
|
+
## `TokenCache` 用法
|
|
102
|
+
|
|
103
|
+
`login()` 默认自动读写 [`TokenCache`](src/haozhuma/token_cache.py:14)。
|
|
104
|
+
|
|
105
|
+
- 命中缓存:直接复用 `token`
|
|
106
|
+
- 未命中缓存:重新登录并写回缓存
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from haozhuma import TokenCache, login
|
|
110
|
+
|
|
111
|
+
cache = TokenCache("./token_cache.json")
|
|
112
|
+
sms = login("your_user", "your_password", sid="92162", token_cache=cache)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
如果你不想用缓存:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from haozhuma import login
|
|
119
|
+
|
|
120
|
+
sms = login("your_user", "your_password", sid="92162", use_cache=False)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 已有 `token` 的用法
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from haozhuma import from_token
|
|
127
|
+
|
|
128
|
+
sms = from_token("your_token", sid="92162")
|
|
129
|
+
phone = sms.get_phone()
|
|
130
|
+
code = sms.poll_code()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 保留完整响应对象
|
|
134
|
+
|
|
135
|
+
如果你不只要字符串,也可以拿完整结构。
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from haozhuma import login
|
|
139
|
+
|
|
140
|
+
sms = login("your_user", "your_password", sid="92162")
|
|
141
|
+
|
|
142
|
+
phone_info = sms.get_phone_info()
|
|
143
|
+
message = sms.poll_message()
|
|
144
|
+
|
|
145
|
+
print(phone_info.raw)
|
|
146
|
+
print(message.raw)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## `SMSProvider` 抽象
|
|
150
|
+
|
|
151
|
+
[`SMSProvider`](src/haozhuma/provider.py:9) 是统一协议,方便以后接别的接码平台。
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from haozhuma import SMSProvider, login
|
|
155
|
+
|
|
156
|
+
sms: SMSProvider = login("your_user", "your_password", sid="92162")
|
|
157
|
+
phone, code = sms.next_code()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## API 对照
|
|
161
|
+
|
|
162
|
+
| SDK 方法 | 返回值 | 说明 |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `login()` | `HaozhuClient` | 登录并返回可复用对象,默认自动复用 `TokenCache` |
|
|
165
|
+
| `from_token()` | `HaozhuClient` | 用已有 `token` 恢复对象 |
|
|
166
|
+
| `get_phone()` | `str` | 获取号码并保存状态 |
|
|
167
|
+
| `get_phone_info()` | `PhoneResult` | 获取号码完整响应 |
|
|
168
|
+
| `poll_code()` | `str` | 轮询验证码并保存状态 |
|
|
169
|
+
| `poll_message()` | `MessageResult` | 轮询短信完整响应 |
|
|
170
|
+
| `next_code()` | `(phone, code)` | 一行完成取号 + 取码 |
|
|
171
|
+
| `release()` | `dict` | 释放当前号码 |
|
|
172
|
+
| `blacklist()` | `dict` | 拉黑当前号码 |
|
|
173
|
+
| `TokenCache` | `class` | 管理登录态缓存 |
|
|
174
|
+
| `SMSProvider` | `Protocol` | 接码平台统一抽象 |
|
|
175
|
+
|
|
176
|
+
## 当前目录说明
|
|
177
|
+
|
|
178
|
+
| 文件 | 是否保留 | 说明 |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `src/haozhuma/client.py` | 保留 | 核心客户端 |
|
|
181
|
+
| `src/haozhuma/token_cache.py` | 保留 | token 缓存 |
|
|
182
|
+
| `src/haozhuma/provider.py` | 保留 | 平台抽象协议 |
|
|
183
|
+
| `src/haozhuma/models.py` | 保留 | 响应模型 |
|
|
184
|
+
| `src/haozhuma/exceptions.py` | 保留 | 异常定义 |
|
|
185
|
+
| `src/haozhuma/__init__.py` | 保留 | 对外导出 |
|
|
186
|
+
| `pyproject.toml` | 保留 | `pip install` 打包入口 |
|
|
187
|
+
| `README.md` | 保留 | 用法文档 |
|
|
188
|
+
|
|
189
|
+
## 免责声明
|
|
190
|
+
|
|
191
|
+
本模块仅用于已授权项目的验证码接收、测试环境验证和自动化流程开发。使用者需自行确认账号、项目和接口调用均符合平台规则与当地法律。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
haozhuma/__init__.py,sha256=gx4Ui8CNa9C_NmCz7hL_X2yOmjioj79vJfCxstWpv7g,582
|
|
2
|
+
haozhuma/client.py,sha256=7ug1WMjgsgSpsjNU0icHQUo4c9hyhPf4mkrEHIwTaiM,16020
|
|
3
|
+
haozhuma/exceptions.py,sha256=XkeX5dN-5DcvKuh3nHijIBqXucSpIkGn_DRW9RK_WvI,1143
|
|
4
|
+
haozhuma/models.py,sha256=4tlL3LKIg09dz1tbItJtiUG0XL4TLSh_XQJNsyBUQDA,2395
|
|
5
|
+
haozhuma/provider.py,sha256=KLn8AXG8dgtvtISIvLtOAf4M_4-km-rSxyv0Tvi54Es,1641
|
|
6
|
+
haozhuma/token_cache.py,sha256=m5Gb-vp9axlopeCbxeUnEq1PpI4llAgPFr8Fde1rZJY,2878
|
|
7
|
+
haozhuma-0.1.0.dist-info/METADATA,sha256=idTCLF7uVksnUgMGmSqIT4LD03eJz-dJpMGgIX4fQ5o,5228
|
|
8
|
+
haozhuma-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
haozhuma-0.1.0.dist-info/top_level.txt,sha256=ls6AnC1j0U2-OVa60J0BdgQcZLEdKxRpRjZANXOtw6U,9
|
|
10
|
+
haozhuma-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
haozhuma
|