aioqzone 1.8.3.dev1__tar.gz → 1.8.5.dev1__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 (52) hide show
  1. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/PKG-INFO +1 -1
  2. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/pyproject.toml +1 -1
  3. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/h5/model.py +39 -12
  4. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/request.py +25 -3
  5. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/response.py +22 -16
  6. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/regex.py +1 -1
  7. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/web.py +2 -7
  8. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/LICENSE +0 -0
  9. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/README.md +0 -0
  10. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/__init__.py +0 -0
  11. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/__init__.py +0 -0
  12. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/h5/__init__.py +0 -0
  13. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/login/__init__.py +0 -0
  14. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/login/_base.py +0 -0
  15. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/exception.py +0 -0
  16. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/message.py +0 -0
  17. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/__init__.py +0 -0
  18. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/__init__.py +0 -0
  19. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/feed.py +0 -0
  20. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/profile.py +0 -0
  21. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/protocol/__init__.py +0 -0
  22. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/protocol/config.py +0 -0
  23. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/protocol/entity.py +0 -0
  24. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/__init__.py +0 -0
  25. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/entity.py +0 -0
  26. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/retry.py +0 -0
  27. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/time.py +0 -0
  28. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/__init__.py +0 -0
  29. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/base.py +0 -0
  30. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/constant.py +0 -0
  31. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/exception.py +0 -0
  32. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/message.py +0 -0
  33. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/py.typed +0 -0
  34. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/qr/__init__.py +0 -0
  35. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/qr/type.py +0 -0
  36. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/type.py +0 -0
  37. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/__init__.py +0 -0
  38. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/_model.py +0 -0
  39. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/__init__.py +0 -0
  40. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/_model.py +0 -0
  41. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/capsess.py +0 -0
  42. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/pil_utils.py +0 -0
  43. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/select/__init__.py +0 -0
  44. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/select/_types.py +0 -0
  45. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/slide/__init__.py +0 -0
  46. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/slide/_types.py +0 -0
  47. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/encrypt.py +0 -0
  48. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/h5.py +0 -0
  49. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/encrypt.py +0 -0
  50. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/iter.py +0 -0
  51. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/jsjson.py +0 -0
  52. {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/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.3.dev1
3
+ Version: 1.8.5.dev1
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.3.dev1"
3
+ version = "1.8.5.dev1"
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"
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from os import PathLike
2
3
 
3
4
  from pydantic import ValidationError
4
5
  from tenacity import AsyncRetrying, TryAgain, after_log, stop_after_attempt
@@ -177,7 +178,7 @@ class QzoneH5API:
177
178
  async def publish_mood(
178
179
  self,
179
180
  content: str,
180
- photos: t.Optional[t.List[PhotoData]] = None,
181
+ photos: t.Optional[t.Sequence[t.Union[PhotoData, PicInfo]]] = None,
181
182
  sync_weibo=False,
182
183
  ugc_right: UgcRight = UgcRight.all,
183
184
  ) -> PublishMoodResp:
@@ -189,21 +190,47 @@ class QzoneH5API:
189
190
  :param ugc_right: access right, default to "Available to Everyone".
190
191
  """
191
192
  photos = photos or []
192
- return await self.call(PublishMoodApi(params=PublishMoodParams.model_validate(locals())))
193
+ return await self.call(
194
+ PublishMoodApi(params=PublishMoodParams.model_validate(locals(), from_attributes=True))
195
+ )
193
196
 
194
197
  async def upload_pic(
195
- self, picture: bytes, width: int, height: int, quality: int
198
+ self,
199
+ picture: t.Union[bytes, str, PathLike, t.IO[bytes]],
200
+ width: t.Optional[int] = None,
201
+ height: t.Optional[int] = None,
202
+ quality: t.Union[int, float] = 70,
196
203
  ) -> UploadPicResponse:
197
- return await self.call(
198
- UploadPicApi(
199
- params=UploadPicParams(
200
- picture=picture,
201
- hd_width=width,
202
- hd_height=height,
203
- hd_quality=quality,
204
- ),
204
+ """
205
+ .. versionchanged:: 1.8.5
206
+
207
+ In version <= 1.8.4, user is responsible for compressing a image and this api
208
+ encode the :obj:`picture` with Base64 and send it to Qzone _ASIS_.
209
+
210
+ Since version 1.8.5, we recognize a compressed image by :obj:`width` and :obj:`height`
211
+ parameters. If :obj:`width` and :obj:`height` is provided, this API will keep the former
212
+ behavior. If not provided, the image will be compressed with the given quality.
213
+ """
214
+ if isinstance(quality, float):
215
+ if quality < 1:
216
+ quality *= 100
217
+ quality = int(quality)
218
+
219
+ assert 0 < quality <= 100
220
+
221
+ if isinstance(picture, (str, PathLike, t.IO)):
222
+ params = UploadPicParams.from_image(picture, quality)
223
+ elif (width is None) or (height is None):
224
+ params = UploadPicParams.from_bytes(picture, quality)
225
+ else:
226
+ params = UploadPicParams(
227
+ picture=picture,
228
+ hd_width=width,
229
+ hd_height=height,
230
+ hd_quality=quality,
205
231
  )
206
- )
232
+
233
+ return await self.call(UploadPicApi(params=params))
207
234
 
208
235
  async def preupload_photos(
209
236
  self, upload_pics: t.List[UploadPicResponse], cur_num=0, upload_hd=False
@@ -1,10 +1,12 @@
1
1
  import typing as t
2
2
  from base64 import b64encode
3
+ from io import BytesIO
3
4
  from math import floor
5
+ from os import PathLike
4
6
  from time import time
5
7
 
6
- from pydantic import Base64Encoder, BaseModel, EncodedBytes, Field, field_serializer
7
- from typing_extensions import Annotated
8
+ from pydantic import BaseModel, Field, field_serializer, field_validator
9
+ from typing_extensions import Buffer
8
10
 
9
11
  from aioqzone.utils.time import time_ms
10
12
 
@@ -164,7 +166,7 @@ class DeleteUgcParams(QzoneRequestParams):
164
166
 
165
167
  class UploadPicParams(QzoneRequestParams):
166
168
  uin_fields = ("uin",)
167
- picture: Annotated[bytes, EncodedBytes(Base64Encoder)]
169
+ picture: bytes
168
170
  hd_height: int
169
171
  hd_width: int
170
172
  hd_quality: int = 70
@@ -180,6 +182,26 @@ class UploadPicParams(QzoneRequestParams):
180
182
  Exif_CameraModel: str = ""
181
183
  Exif_Time: str = ""
182
184
 
185
+ @field_serializer("picture", return_type=str)
186
+ def b64_picture(self, picture: t.ByteString) -> str:
187
+ return b64encode(picture).decode()
188
+
189
+ @classmethod
190
+ def from_image(cls, image_file: t.Union[str, PathLike, t.IO[bytes]], quality=70):
191
+ import PIL.Image as image
192
+
193
+ with image.open(image_file) as f:
194
+ buf = BytesIO()
195
+ f.save(buf, "JPEG", quality=quality)
196
+ return cls(
197
+ picture=buf.getvalue(), hd_height=f.height, hd_width=f.width, hd_quality=quality
198
+ )
199
+
200
+ @classmethod
201
+ def from_bytes(cls, image_bytes: Buffer, quality=70):
202
+ buf = BytesIO(image_bytes)
203
+ return cls.from_image(buf, quality=quality)
204
+
183
205
 
184
206
  class PhotosPreuploadParams(QzoneRequestParams):
185
207
  uin_fields = ("uin",)
@@ -33,13 +33,14 @@ __all__ = [
33
33
  "UploadPicResponse",
34
34
  "PhotosPreuploadResponse",
35
35
  "FeedData",
36
+ "PicInfo",
36
37
  "ProfileFeedData",
37
38
  ]
38
39
 
39
40
 
40
41
  class QzoneResponse(BaseModel):
41
42
  _errno_key: t.ClassVar[t.Union[str, AliasPath, AliasChoices, None]] = AliasChoices(
42
- "code", "ret", "err"
43
+ "code", "ret", "err", "error"
43
44
  )
44
45
  _msg_key: t.ClassVar[t.Union[str, AliasPath, AliasChoices, None]] = AliasChoices(
45
46
  "message", "msg"
@@ -55,18 +56,17 @@ class QzoneResponse(BaseModel):
55
56
 
56
57
  :return: Self
57
58
  """
58
- if cls._errno_key and cls._msg_key:
59
59
 
60
- class response_header(BaseModel):
61
- status: int = Field(validation_alias=cls._errno_key)
62
- message: str = Field(default="", validation_alias=cls._msg_key)
60
+ class response_header(BaseModel):
61
+ status: int = Field(default=0, validation_alias=cls._errno_key)
62
+ message: str = Field(default="", validation_alias=cls._msg_key)
63
63
 
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)
64
+ header = response_header.model_validate(obj)
65
+ if header.status != 0:
66
+ if header.message:
67
+ raise QzoneError(header.status, header.message, robj=obj)
68
+ else:
69
+ raise QzoneError(header.status, robj=obj)
70
70
 
71
71
  if cls._data_key is None:
72
72
  return cls.model_validate(obj)
@@ -208,7 +208,7 @@ class ProfilePagePesp(QzoneResponse):
208
208
  return cls(
209
209
  info=QzoneInfo.from_response_object(obj["info"]), # type: ignore
210
210
  feedpage=ProfileResp.from_response_object(obj["feedpage"]), # type: ignore
211
- qzonetoken=obj["qzonetoken"],
211
+ qzonetoken=obj["qzonetoken"], # type: ignore
212
212
  )
213
213
 
214
214
 
@@ -239,7 +239,8 @@ class DeleteUgcResp(QzoneResponse):
239
239
 
240
240
 
241
241
  class UploadPicResponse(QzoneResponse):
242
- _errno_key = None
242
+ _data_key = None
243
+
243
244
  filelen: int
244
245
  filemd5: str
245
246
 
@@ -250,7 +251,9 @@ class UploadPicResponse(QzoneResponse):
250
251
  return json_loads(m.group(1))
251
252
 
252
253
 
253
- class PicInfo(BaseModel):
254
+ class PicInfo(QzoneResponse):
255
+ _data_key = None
256
+
254
257
  pre: HttpUrl
255
258
  url: HttpUrl
256
259
  sloc: str
@@ -261,11 +264,14 @@ class PicInfo(BaseModel):
261
264
 
262
265
 
263
266
  class PhotosPreuploadResponse(QzoneResponse):
264
- _errno_key = None
267
+ _data_key = None
265
268
  photos: t.List[PicInfo] = Field(default_factory=list)
266
269
 
267
270
  @classmethod
268
271
  async def response_to_object(cls, response: ClientResponse):
269
272
  m = response_callback.search(await response.text())
270
273
  assert m
271
- return dict(photos=json_loads(m.group(1)))
274
+
275
+ picinfos = json_loads(m.group(1))
276
+ assert isinstance(picinfos, list)
277
+ return dict(photos=[PicInfo.from_response_object(info["picinfo"]) for info in picinfos]) # type: ignore
@@ -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
 
@@ -104,15 +104,10 @@ class _UpHookMixin:
104
104
 
105
105
  class UpWebLogin(LoginBase[UpWebSession], _UpHookMixin):
106
106
  """
107
- .. versionchanged:: 0.12.4
108
-
109
- `TeaEncoder` is used as the default password encoder. A `legacy_encoder` paramater is added to force
110
- using the former `NodeEncoder`. It can also be configured by set :envvar:`AIOQZONE_PWDENCODER` to "node".
111
- Note that the paramater in code, i.e. `legacy_encoder`, takes precedence.
112
-
113
107
  .. versionchanged:: 0.13.0.dev1
114
108
 
115
- `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.
116
111
  """
117
112
 
118
113
  def __init__(
File without changes
File without changes