aioqzone 1.8.2.dev3__tar.gz → 1.8.4.dev3__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.
Files changed (53) hide show
  1. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/PKG-INFO +1 -1
  2. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/pyproject.toml +1 -1
  3. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/api/request.py +6 -3
  4. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/api/response.py +21 -16
  5. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/protocol/config.py +2 -0
  6. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/utils/regex.py +1 -1
  7. aioqzone-1.8.4.dev3/src/qqqr/__init__.py +4 -0
  8. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/qr/__init__.py +50 -8
  9. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/web.py +11 -11
  10. aioqzone-1.8.2.dev3/src/qqqr/__init__.py +0 -4
  11. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/LICENSE +0 -0
  12. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/README.md +0 -0
  13. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/__init__.py +0 -0
  14. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/api/__init__.py +0 -0
  15. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/api/h5/__init__.py +0 -0
  16. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/api/h5/model.py +0 -0
  17. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/api/login/__init__.py +0 -0
  18. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/api/login/_base.py +0 -0
  19. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/exception.py +0 -0
  20. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/message.py +0 -0
  21. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/__init__.py +0 -0
  22. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/api/__init__.py +0 -0
  23. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/api/feed.py +0 -0
  24. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/api/profile.py +0 -0
  25. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/protocol/__init__.py +0 -0
  26. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/model/protocol/entity.py +0 -0
  27. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/utils/__init__.py +0 -0
  28. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/utils/entity.py +0 -0
  29. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/utils/retry.py +0 -0
  30. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/aioqzone/utils/time.py +0 -0
  31. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/base.py +0 -0
  32. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/constant.py +0 -0
  33. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/exception.py +0 -0
  34. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/message.py +0 -0
  35. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/py.typed +0 -0
  36. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/qr/type.py +0 -0
  37. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/type.py +0 -0
  38. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/__init__.py +0 -0
  39. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/_model.py +0 -0
  40. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/__init__.py +0 -0
  41. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/_model.py +0 -0
  42. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/capsess.py +0 -0
  43. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/pil_utils.py +0 -0
  44. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/select/__init__.py +0 -0
  45. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/select/_types.py +0 -0
  46. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/slide/__init__.py +0 -0
  47. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/captcha/slide/_types.py +0 -0
  48. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/encrypt.py +0 -0
  49. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/up/h5.py +0 -0
  50. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/utils/encrypt.py +0 -0
  51. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/utils/iter.py +0 -0
  52. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/utils/jsjson.py +0 -0
  53. {aioqzone-1.8.2.dev3 → aioqzone-1.8.4.dev3}/src/qqqr/utils/net.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aioqzone
3
- Version: 1.8.2.dev3
3
+ Version: 1.8.4.dev3
4
4
  Summary: A Python wrapper for Qzone login and H5 APIs.
5
5
  Home-page: https://github.com/aioqzone/aioqzone
6
6
  License: AGPL-3.0
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aioqzone"
3
- version = "1.8.2.dev3"
3
+ version = "1.8.4.dev3"
4
4
  description = "A Python wrapper for Qzone login and H5 APIs."
5
5
  authors = ["aioqzone <zzzzss990315@gmail.com>"]
6
6
  license = "AGPL-3.0"
@@ -3,8 +3,7 @@ from base64 import b64encode
3
3
  from math import floor
4
4
  from time import time
5
5
 
6
- from pydantic import Base64Encoder, BaseModel, EncodedBytes, Field, field_serializer
7
- from typing_extensions import Annotated
6
+ from pydantic import BaseModel, Field, field_serializer
8
7
 
9
8
  from aioqzone.utils.time import time_ms
10
9
 
@@ -164,7 +163,7 @@ class DeleteUgcParams(QzoneRequestParams):
164
163
 
165
164
  class UploadPicParams(QzoneRequestParams):
166
165
  uin_fields = ("uin",)
167
- picture: Annotated[bytes, EncodedBytes(Base64Encoder)]
166
+ picture: bytes
168
167
  hd_height: int
169
168
  hd_width: int
170
169
  hd_quality: int = 70
@@ -180,6 +179,10 @@ class UploadPicParams(QzoneRequestParams):
180
179
  Exif_CameraModel: str = ""
181
180
  Exif_Time: str = ""
182
181
 
182
+ @field_serializer("picture", return_type=str)
183
+ def b64_picture(self, picture: t.ByteString) -> str:
184
+ return b64encode(picture).decode()
185
+
183
186
 
184
187
  class PhotosPreuploadParams(QzoneRequestParams):
185
188
  uin_fields = ("uin",)
@@ -39,7 +39,7 @@ __all__ = [
39
39
 
40
40
  class QzoneResponse(BaseModel):
41
41
  _errno_key: t.ClassVar[t.Union[str, AliasPath, AliasChoices, None]] = AliasChoices(
42
- "code", "ret", "err"
42
+ "code", "ret", "err", "error"
43
43
  )
44
44
  _msg_key: t.ClassVar[t.Union[str, AliasPath, AliasChoices, None]] = AliasChoices(
45
45
  "message", "msg"
@@ -55,18 +55,17 @@ class QzoneResponse(BaseModel):
55
55
 
56
56
  :return: Self
57
57
  """
58
- if cls._errno_key and cls._msg_key:
59
58
 
60
- class response_header(BaseModel):
61
- status: int = Field(validation_alias=cls._errno_key)
62
- message: str = Field(default="", validation_alias=cls._msg_key)
59
+ class response_header(BaseModel):
60
+ status: int = Field(default=0, validation_alias=cls._errno_key)
61
+ message: str = Field(default="", validation_alias=cls._msg_key)
63
62
 
64
- header = response_header.model_validate(obj)
65
- if header.status != 0:
66
- if header.message:
67
- raise QzoneError(header.status, header.message, robj=header)
68
- else:
69
- raise QzoneError(header.status, robj=header)
63
+ header = response_header.model_validate(obj)
64
+ if header.status != 0:
65
+ if header.message:
66
+ raise QzoneError(header.status, header.message, robj=obj)
67
+ else:
68
+ raise QzoneError(header.status, robj=obj)
70
69
 
71
70
  if cls._data_key is None:
72
71
  return cls.model_validate(obj)
@@ -208,7 +207,7 @@ class ProfilePagePesp(QzoneResponse):
208
207
  return cls(
209
208
  info=QzoneInfo.from_response_object(obj["info"]), # type: ignore
210
209
  feedpage=ProfileResp.from_response_object(obj["feedpage"]), # type: ignore
211
- qzonetoken=obj["qzonetoken"],
210
+ qzonetoken=obj["qzonetoken"], # type: ignore
212
211
  )
213
212
 
214
213
 
@@ -239,7 +238,8 @@ class DeleteUgcResp(QzoneResponse):
239
238
 
240
239
 
241
240
  class UploadPicResponse(QzoneResponse):
242
- _errno_key = None
241
+ _data_key = None
242
+
243
243
  filelen: int
244
244
  filemd5: str
245
245
 
@@ -250,7 +250,9 @@ class UploadPicResponse(QzoneResponse):
250
250
  return json_loads(m.group(1))
251
251
 
252
252
 
253
- class PicInfo(BaseModel):
253
+ class PicInfo(QzoneResponse):
254
+ _data_key = None
255
+
254
256
  pre: HttpUrl
255
257
  url: HttpUrl
256
258
  sloc: str
@@ -261,11 +263,14 @@ class PicInfo(BaseModel):
261
263
 
262
264
 
263
265
  class PhotosPreuploadResponse(QzoneResponse):
264
- _errno_key = None
266
+ _data_key = None
265
267
  photos: t.List[PicInfo] = Field(default_factory=list)
266
268
 
267
269
  @classmethod
268
270
  async def response_to_object(cls, response: ClientResponse):
269
271
  m = response_callback.search(await response.text())
270
272
  assert m
271
- return dict(photos=json_loads(m.group(1)))
273
+
274
+ picinfos = json_loads(m.group(1))
275
+ assert isinstance(picinfos, list)
276
+ return dict(photos=[PicInfo.from_response_object(info["picinfo"]) for info in picinfos])
@@ -26,3 +26,5 @@ class QrLoginConfig(LoginConfig):
26
26
  """Maximum QR code refresh times."""
27
27
  poll_freq: float = 3
28
28
  """QR status polling interval."""
29
+ no_push: bool = False
30
+ """Do not try to push the QR code to user's client."""
@@ -5,7 +5,7 @@ some patterns for matching html and so on.
5
5
  import re
6
6
 
7
7
  # use this to match qzone api response
8
- response_callback = re.compile(r"callback\(\s*(\{.*\})\s*\)", re.S | re.I)
8
+ response_callback = re.compile(r"callback\(\s*([\{\[].*[\}\]])\s*\)", re.S | re.I)
9
9
  # use this to get unikey & curkey of a html
10
10
  uni_cur_key = re.compile(r'data-unikey="([^"]*)"[^d]*data-curkey="([^"]*)"')
11
11
 
@@ -0,0 +1,4 @@
1
+ """
2
+ QQQR is an API-level simulation of Qzone web login process. Currently this package
3
+ includes QR login and password login. A captcha verifier is also contained to pass TDC.
4
+ """
@@ -28,13 +28,17 @@ PTLOGIN2 = URL("https://ptlogin2.qq.com")
28
28
 
29
29
  @dataclass(unsafe_hash=True)
30
30
  class QR:
31
+ """Class :class:`QR` represents a QR code."""
32
+
31
33
  png: t.Optional[bytes]
32
- """If None, the QR is pushed to user's client."""
34
+ """QR code content. If None, the QR is pushed to user's client."""
33
35
  sig: str
34
36
  expired: bool = False
37
+ """Whether the QR code is expired."""
35
38
 
36
39
  @property
37
40
  def pushed(self):
41
+ """Whether the QR code is pushed to user's client."""
38
42
  return self.png is None
39
43
 
40
44
 
@@ -49,9 +53,12 @@ class QrSession(LoginSession):
49
53
  ) -> None:
50
54
  super().__init__(login_sig=login_sig, create_time=create_time)
51
55
  self.refreshed = refresh_times
56
+ """QR code refresh times counter."""
52
57
  self.current_qr = first_qr
58
+ """A :class:`QrSession` keeps a :class:`QR` object as current QR code."""
53
59
 
54
60
  def new_qr(self, qr: QR):
61
+ """Add a new QR code to this session."""
55
62
  self.current_qr.expired = True
56
63
  self.current_qr = qr
57
64
  self.refreshed += 1
@@ -61,14 +68,36 @@ class _QrHookMixin:
61
68
  def __init__(self, *args, **kwds) -> None:
62
69
  super().__init__(*args, **kwds)
63
70
  self.qr_fetched = MT.qr_fetched.with_timeout(60)
71
+ """This emitter is triggered when a QR code is fetched."""
64
72
  self.qr_cancelled = MT.qr_cancelled()
73
+ """This emitter is triggered when QR login is cancelled."""
65
74
  self.cancel = asyncio.Event()
75
+ """Async-event indicating whether the loop should cancel the QR login."""
66
76
  self.refresh = asyncio.Event()
77
+ """Async-event indicating whether the loop should refresh the QR code immediately."""
67
78
 
68
79
 
69
80
  class QrLogin(LoginBase[QrSession], _QrHookMixin):
70
- async def new(self) -> QrSession:
81
+ async def new(self, no_push=False) -> QrSession:
82
+ """Create a :class:`QrSession`. This method will:
83
+
84
+ 1. GET ``xlogin`` url to get ``pt_login_sig`` cookie;
85
+
86
+ #. Try "quick login" (the QR code is pushed to user's client);
87
+
88
+ #. Whether the QR code is pushed or not, a :class:`QR` object is created
89
+ and is hold by the returned :class:`QrSession`.
90
+
91
+ :param no_push: Do not try to push the QR code to user's client.
92
+ :return: a :class:`QrSession`
93
+
94
+ .. versionchanged:: 1.8.3
95
+
96
+ Added :obj:`no_push` param.
97
+ """
71
98
  login_sig = await self._pt_login_sig()
99
+ if no_push:
100
+ return QrSession(await self.show(), login_sig=login_sig)
72
101
 
73
102
  cookie = self.client.cookie_jar.filter_cookies(PTLOGIN2).get("pt_guid_sig")
74
103
  push_qr = False
@@ -88,9 +117,10 @@ class QrLogin(LoginBase[QrSession], _QrHookMixin):
88
117
  return QrSession(await self.show(push_qr), login_sig=login_sig)
89
118
 
90
119
  async def show(self, push_qr=False) -> QR:
91
- """``ptqrshow`` api.
120
+ """This method will call ``ptqrshow`` api and wrap the response QR bytes into :class:`QR`.
92
121
 
93
122
  :param push_qr: push QR to mobile client.
123
+ :return: a :class:`QR` object.
94
124
  """
95
125
  data = {
96
126
  "appid": self.app.appid,
@@ -170,26 +200,37 @@ class QrLogin(LoginBase[QrSession], _QrHookMixin):
170
200
  *,
171
201
  refresh_times: int = 6,
172
202
  poll_freq: float = 3,
203
+ no_push=False,
173
204
  ):
174
- """Loop until cookie is returned or max `refresh_times` exceeds.
205
+ """Loop until cookie is returned or max :obj:`refresh_times` exceeds.
206
+
175
207
  - This method will emit :obj:`.qr_fetched` event if a new qrcode is fetched.
176
- - If qr is not scanned after `refresh_times`, it will raise :exc:`UserTimeout`.
208
+
209
+ - If the QR code is not scanned after :obj:`refresh_times`,
210
+ it will raise :exc:`~qqqr.exception.UserTimeout`.
211
+
177
212
  - If :obj:`.refresh` is set, it will refresh qrcode at once without increasing expire counter.
178
- - If :obj:`.cancel` is set, it will raise :exc:`UserBreak` before next polling.
213
+
214
+ - If :obj:`.cancel` is set, it will raise :exc:`~qqqr.exception.UserBreak` before next polling.
179
215
 
180
216
  :meta public:
181
217
  :param refresh_times: max qr expire times.
182
218
  :param poll_freq: interval between two status polling, in seconds, default as 3.
219
+ :param no_push: Do not try to push the QR code to user's client.
183
220
 
184
- :raise `UserTimeout`: if qr is not scanned after `refresh_times` expires.
221
+ :raise `UserTimeout`: if the QR code is not scanned after :obj:`refresh_times` expires.
185
222
  :raise `UserBreak`: if :obj:`.cancel` is set.
223
+
224
+ .. versionchanged:: 1.8.3
225
+
226
+ Added :obj:`no_push` param.
186
227
  """
187
228
  self.refresh.clear()
188
229
  self.cancel.clear()
189
230
 
190
231
  cnt_expire = 0
191
232
  renew = False
192
- sess = await self.new()
233
+ sess = await self.new(no_push)
193
234
 
194
235
  while cnt_expire < refresh_times:
195
236
  # BUG: should we wrap hook errors here?
@@ -206,6 +247,7 @@ class QrLogin(LoginBase[QrSession], _QrHookMixin):
206
247
  await asyncio.sleep(poll_freq)
207
248
  stat = await self.poll(sess)
208
249
  if stat.code == StatusCode.Expired:
250
+ sess.current_qr.expired = True
209
251
  cnt_expire += 1
210
252
  break
211
253
  elif stat.code == StatusCode.Authenticated:
@@ -99,19 +99,15 @@ class _UpHookMixin:
99
99
  def __init__(self, *args, **kwds) -> None:
100
100
  super().__init__(*args, **kwds)
101
101
  self.sms_code_input = MT.sms_code_input.with_timeout(60)
102
+ """This emitter is triggered when SMS verify code is needed during login."""
102
103
 
103
104
 
104
105
  class UpWebLogin(LoginBase[UpWebSession], _UpHookMixin):
105
106
  """
106
- .. versionchanged:: 0.12.4
107
-
108
- `TeaEncoder` is used as the default password encoder. A `legacy_encoder` paramater is added to force
109
- using the former `NodeEncoder`. It can also be configured by set :envvar:`AIOQZONE_PWDENCODER` to "node".
110
- Note that the paramater in code, i.e. `legacy_encoder`, takes precedence.
111
-
112
107
  .. versionchanged:: 0.13.0.dev1
113
108
 
114
- `TeaEncoder` is the only encoder. ``NodeEncoder`` is removed.
109
+ :class:`~qqqr.up.encrypt.TeaEncoder` is the unique :class:`~qqqr.up.encrypt.PasswdEncoder`.
110
+ ``NodeEncoder`` is removed.
115
111
  """
116
112
 
117
113
  def __init__(
@@ -132,9 +128,8 @@ class UpWebLogin(LoginBase[UpWebSession], _UpHookMixin):
132
128
  self.client, self.app.appid, str(self.login_page_url), fake_ip=fake_ip
133
129
  )
134
130
 
135
- async def new(self):
136
- """Create a :class:`UpWebSession`. This will call `check` api of Qzone, and receive result
137
- about whether this login needs a captcha, sms verification, etc.
131
+ async def new(self) -> UpWebSession:
132
+ """Create a :class:`UpWebSession`. This will trigger a GET to ``xlogin`` url.
138
133
 
139
134
  :raise `aiohttp.ClientResponseError`: if response status != 200
140
135
 
@@ -143,6 +138,11 @@ class UpWebLogin(LoginBase[UpWebSession], _UpHookMixin):
143
138
  return UpWebSession(await self._pt_login_sig())
144
139
 
145
140
  async def check(self, sess: UpWebSession):
141
+ """This will call ``check`` api of Qzone, and receive result about
142
+ whether this login needs a captcha, sms verification, etc.
143
+
144
+ :param sess: Session got from :meth:`~UpWebLogin.new`.
145
+ """
146
146
  data = {
147
147
  "regmaster": "",
148
148
  "pt_tea": 2,
@@ -217,7 +217,7 @@ class UpWebLogin(LoginBase[UpWebSession], _UpHookMixin):
217
217
  data.update(const)
218
218
  return data
219
219
 
220
- async def try_login(self, sess: UpWebSession):
220
+ async def try_login(self, sess: UpWebSession) -> LoginResp:
221
221
  """
222
222
  Check if current session meets the login condition.
223
223
  It takes a session object and returns response of this try.
@@ -1,4 +0,0 @@
1
- """
2
- QQQR is a simulation of Qzone web login process. Including QR login and password login.
3
- A captcha verifier is also contained to pass TDC.
4
- """
File without changes
File without changes