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.
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/PKG-INFO +1 -1
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/pyproject.toml +1 -1
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/h5/model.py +39 -12
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/request.py +25 -3
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/response.py +22 -16
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/regex.py +1 -1
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/web.py +2 -7
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/LICENSE +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/README.md +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/h5/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/login/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/api/login/_base.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/exception.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/message.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/feed.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/api/profile.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/protocol/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/protocol/config.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/model/protocol/entity.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/entity.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/retry.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/aioqzone/utils/time.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/base.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/constant.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/exception.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/message.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/py.typed +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/qr/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/qr/type.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/type.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/_model.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/_model.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/capsess.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/pil_utils.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/select/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/select/_types.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/slide/__init__.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/captcha/slide/_types.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/encrypt.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/up/h5.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/encrypt.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/iter.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/jsjson.py +0 -0
- {aioqzone-1.8.3.dev1 → aioqzone-1.8.5.dev1}/src/qqqr/utils/net.py +0 -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.
|
|
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(
|
|
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,
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
7
|
-
from typing_extensions import
|
|
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:
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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*(\{
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|