aioqzone 1.9.8.dev1__tar.gz → 1.10.2.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 (54) hide show
  1. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/PKG-INFO +3 -3
  2. aioqzone-1.10.2.dev1/pyproject.toml +102 -0
  3. aioqzone-1.9.8.dev1/pyproject.toml → aioqzone-1.10.2.dev1/pyproject.toml.orig +5 -6
  4. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/api/h5/model.py +26 -0
  5. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/api/login/__init__.py +23 -2
  6. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/api/__init__.py +13 -0
  7. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/api/request.py +17 -0
  8. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/api/response.py +116 -0
  9. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/utils/jsjson.py +4 -3
  10. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/README.md +0 -0
  11. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/__init__.py +0 -0
  12. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/api/__init__.py +0 -0
  13. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/api/h5/__init__.py +0 -0
  14. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/api/login/_base.py +0 -0
  15. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/exception.py +0 -0
  16. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/message.py +0 -0
  17. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/__init__.py +0 -0
  18. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/api/feed.py +0 -0
  19. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/api/profile.py +0 -0
  20. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/protocol/__init__.py +0 -0
  21. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/protocol/config.py +0 -0
  22. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/model/protocol/entity.py +0 -0
  23. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/utils/__init__.py +0 -0
  24. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/utils/entity.py +0 -0
  25. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/utils/regex.py +0 -0
  26. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/utils/retry.py +0 -0
  27. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/aioqzone/utils/time.py +0 -0
  28. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/__init__.py +0 -0
  29. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/base.py +0 -0
  30. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/constant.py +0 -0
  31. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/exception.py +0 -0
  32. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/message.py +0 -0
  33. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/py.typed +0 -0
  34. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/qr/__init__.py +0 -0
  35. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/qr/type.py +0 -0
  36. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/type.py +0 -0
  37. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/__init__.py +0 -0
  38. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/_model.py +0 -0
  39. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/__init__.py +0 -0
  40. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/_model.py +0 -0
  41. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/capsess.py +0 -0
  42. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/click/__init__.py +0 -0
  43. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/click/_types.py +0 -0
  44. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/pil_utils.py +0 -0
  45. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/select/__init__.py +0 -0
  46. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/select/_types.py +0 -0
  47. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/slide/__init__.py +0 -0
  48. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/captcha/slide/_types.py +0 -0
  49. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/encrypt.py +0 -0
  50. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/h5.py +0 -0
  51. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/up/web.py +0 -0
  52. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/utils/encrypt.py +0 -0
  53. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/utils/iter.py +0 -0
  54. {aioqzone-1.9.8.dev1 → aioqzone-1.10.2.dev1}/src/qqqr/utils/net.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioqzone
3
- Version: 1.9.8.dev1
3
+ Version: 1.10.2.dev1
4
4
  Summary: A Python wrapper for Qzone login and H5 APIs.
5
5
  Keywords: qzone-api,autologin,asyncio-spider
6
6
  Author: aioqzone
@@ -15,12 +15,12 @@ Requires-Dist: pydantic-settings>=2.9.0
15
15
  Requires-Dist: rsa>=4.9.1
16
16
  Requires-Dist: tenacity>=8.2.3,<10.0.0
17
17
  Requires-Dist: tylisten>=2.2.1
18
- Requires-Dist: pillow>=11.3.0
18
+ Requires-Dist: pillow>=13.0.0
19
19
  Requires-Dist: pychaosvm~=0.5.4
20
20
  Requires-Dist: slide-tc~=0.1.4 ; extra == 'slide-captcha'
21
21
  Maintainer: JamzumSum
22
22
  Maintainer-email: JamzumSum <22952836+JamzumSum@users.noreply.github.com>
23
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.11
24
24
  Project-URL: Homepage, https://github.com/aioqzone/aioqzone
25
25
  Project-URL: Repository, https://github.com/aioqzone/aioqzone
26
26
  Project-URL: Documentation, https://aioqzone.github.io/aioqzone
@@ -0,0 +1,102 @@
1
+ [project]
2
+ name = "aioqzone"
3
+ version = "1.10.2.dev1"
4
+ description = "A Python wrapper for Qzone login and H5 APIs."
5
+ license = "AGPL-3.0-or-later"
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+ keywords = [
9
+ "qzone-api",
10
+ "autologin",
11
+ "asyncio-spider",
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Libraries :: Python Modules",
17
+ "Typing :: Typed",
18
+ ]
19
+ dependencies = [
20
+ "aiohttp~=3.14",
21
+ "pydantic~=2.12",
22
+ "pydantic-settings>=2.9.0",
23
+ "rsa>=4.9.1",
24
+ "tenacity>=8.2.3,<10.0.0",
25
+ "tylisten>=2.2.1",
26
+ "pillow>=13.0.0",
27
+ "pychaosvm~=0.5.4",
28
+ ]
29
+
30
+ [[project.authors]]
31
+ name = "aioqzone"
32
+
33
+ [[project.maintainers]]
34
+ name = "JamzumSum"
35
+ email = "22952836+JamzumSum@users.noreply.github.com"
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/aioqzone/aioqzone"
39
+ Repository = "https://github.com/aioqzone/aioqzone"
40
+ Documentation = "https://aioqzone.github.io/aioqzone"
41
+ "Bug Tracker" = "https://github.com/aioqzone/aioqzone/issues"
42
+ Discussion = "https://t.me/aioqzone_chatroom"
43
+
44
+ [project.optional-dependencies]
45
+ slide-captcha = ["slide-tc~=0.1.4"]
46
+
47
+ [dependency-groups]
48
+ test = [
49
+ "pytest>=9.0.3",
50
+ "pytest-asyncio~=1.1",
51
+ ]
52
+ dev = [
53
+ "pre-commit",
54
+ "ruff",
55
+ "ty",
56
+ ]
57
+ doc = [
58
+ "Sphinx~=9.0",
59
+ "autodoc-pydantic>=2.2.0",
60
+ "sphinx-autodoc-typehints~=3.0",
61
+ "furo",
62
+ "sphinx-intl",
63
+ "sphinx-inline-tabs",
64
+ ]
65
+
66
+ [tool.uv]
67
+ prerelease = "if-necessary-or-explicit"
68
+
69
+ [tool.uv.sources.pychaosvm]
70
+ index = "aioqzone-index"
71
+
72
+ [tool.uv.sources.slide-tc]
73
+ index = "aioqzone-index"
74
+
75
+ [[tool.uv.index]]
76
+ name = "aioqzone-index"
77
+ url = "https://aioqzone.github.io/aioqzone-index/simple/"
78
+
79
+ [tool.uv.build-backend]
80
+ module-name = [
81
+ "aioqzone",
82
+ "qqqr",
83
+ ]
84
+
85
+ [tool.pytest.ini_options]
86
+ pythonpath = "src"
87
+ log_cli = true
88
+ log_cli_level = "WARNING"
89
+ asyncio_default_fixture_loop_scope = "module"
90
+
91
+ [tool.ruff]
92
+ line-length = 99
93
+
94
+ [tool.ruff.lint]
95
+ ignore = [
96
+ "F403",
97
+ "F405",
98
+ ]
99
+
100
+ [build-system]
101
+ requires = ["uv_build"]
102
+ build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioqzone"
3
- version = "1.9.8.dev1"
3
+ version = "1.10.2.dev1"
4
4
  description = "A Python wrapper for Qzone login and H5 APIs."
5
5
  authors = [{ name = "aioqzone" }]
6
6
  maintainers = [
@@ -8,7 +8,7 @@ maintainers = [
8
8
  ]
9
9
  license = "AGPL-3.0-or-later"
10
10
  readme = "README.md"
11
- requires-python = ">=3.10"
11
+ requires-python = ">=3.11"
12
12
  keywords = ["qzone-api", "autologin", "asyncio-spider"]
13
13
  classifiers = [
14
14
  "Development Status :: 4 - Beta",
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "tenacity>=8.2.3,<10.0.0",
26
26
  # "exceptiongroup>=1.1.1",
27
27
  "tylisten>=2.2.1",
28
- "pillow>=11.3.0",
28
+ "pillow>=13.0.0",
29
29
  "pychaosvm~=0.5.4",
30
30
  ]
31
31
 
@@ -37,15 +37,14 @@ Documentation = "https://aioqzone.github.io/aioqzone"
37
37
  "Discussion" = "https://t.me/aioqzone_chatroom"
38
38
 
39
39
  [dependency-groups]
40
- test = ["pytest>=9.0.3", "pytest-asyncio>=1.1,<1.4"]
40
+ test = ["pytest>=9.0.3", "pytest-asyncio~=1.1"]
41
41
  dev = [
42
42
  "pre-commit",
43
43
  "ruff",
44
44
  "ty",
45
45
  ]
46
46
  doc = [
47
- "Sphinx>=8.1.3; python_version=='3.10'",
48
- "Sphinx~=9.0; python_version>='3.11'",
47
+ "Sphinx~=9.0",
49
48
  "autodoc-pydantic>=2.2.0",
50
49
  "sphinx-autodoc-typehints~=3.0",
51
50
  "furo",
@@ -410,3 +410,29 @@ class QzoneH5API:
410
410
  params=SetTopParams.model_validate(locals()),
411
411
  )
412
412
  )
413
+
414
+ async def get_visitor(self, page: int = 1) -> VisitorResp:
415
+ """Get today / total visitor count.
416
+
417
+ :param page: page number
418
+
419
+ .. note::
420
+
421
+ Contributed by hexadecimal233.
422
+ """
423
+ return await self.call(VisitorApi(params=VisitorParams.model_validate(locals())))
424
+
425
+ async def get_message_board(
426
+ self, hostUin: int, num: int = 10, start: int = 0
427
+ ) -> MessageBoardResp:
428
+ """Get message board messages.
429
+
430
+ :param hostUin: QQ of the profile owner
431
+ :param num: number of messages to fetch
432
+ :param start: start offset
433
+
434
+ .. note::
435
+
436
+ Contributed by hexadecimal233.
437
+ """
438
+ return await self.call(MessageBoardApi(params=MessageBoardParams.model_validate(locals())))
@@ -8,7 +8,14 @@ import logging
8
8
  import typing as t
9
9
 
10
10
  from aiohttp import ClientError
11
- from tenacity import TryAgain, retry, retry_if_exception_type, stop_after_attempt, wait_exponential
11
+ from tenacity import (
12
+ RetryCallState,
13
+ TryAgain,
14
+ retry,
15
+ retry_if_exception_type,
16
+ stop_after_attempt,
17
+ wait_exponential,
18
+ )
12
19
  from tylisten import FutureStore
13
20
  from typing_extensions import Self
14
21
 
@@ -31,6 +38,20 @@ __all__ = [
31
38
  ]
32
39
 
33
40
 
41
+ def _reraise_last_attempt(retry_state: RetryCallState) -> t.NoReturn:
42
+ """Re-raise the last attempt exception instead of tenacity's ``RetryError``.
43
+
44
+ tenacity >= 9.2.1 unwraps ``TryAgain`` to its cause when ``reraise=True``,
45
+ which would surface the underlying ``ClientError`` (or ``GeneratorExit``)
46
+ rather than the documented :exc:`TryAgain`. Re-raising the attempt's own
47
+ exception keeps the public contract stable across tenacity versions.
48
+ """
49
+ outcome = retry_state.outcome
50
+ if outcome is None:
51
+ raise RuntimeError("retry outcome is unexpectedly None")
52
+ raise outcome.result()
53
+
54
+
34
55
  @t.final
35
56
  class ConstLoginMan(Loginable):
36
57
  """A basic login manager which uses external provided cookie."""
@@ -99,7 +120,7 @@ class UpLoginManager(Loginable):
99
120
  stop=stop_after_attempt(2),
100
121
  wait=wait_exponential(min=1, max=6),
101
122
  retry=retry_if_exception_type(TryAgain),
102
- reraise=True,
123
+ retry_error_callback=_reraise_last_attempt,
103
124
  )
104
125
  async def _new_cookie(self) -> t.Dict[str, str]:
105
126
  """
@@ -197,3 +197,16 @@ class SetTopApi(QzoneApi[SetTopParams, SingleReturnResp]):
197
197
  http_method: t.ClassVar[TyHttpMethod] = "POST"
198
198
  host: t.ClassVar[str] = "user.qzone.qq.com"
199
199
  path: t.ClassVar[str] = "proxy/domain/ic2.qzone.qq.com/cgi-bin/feeds/cgi_settopfeed"
200
+
201
+
202
+ class VisitorApi(QzoneApi[VisitorParams, VisitorResp]):
203
+ response: t.ClassVar = VisitorResp
204
+ http_method: t.ClassVar[TyHttpMethod] = "GET"
205
+ path: t.ClassVar[str] = "/proxy/domain/g.qzone.qq.com/cgi-bin/friendshow/cgi_get_visitor_more"
206
+
207
+
208
+ class MessageBoardApi(QzoneApi[MessageBoardParams, MessageBoardResp]):
209
+ response: t.ClassVar = MessageBoardResp
210
+ http_method: t.ClassVar[TyHttpMethod] = "GET"
211
+ host: t.ClassVar[str] = "https://user.qzone.qq.com"
212
+ path: t.ClassVar[str] = "/proxy/domain/m.qzone.qq.com/cgi-bin/new/get_msgb"
@@ -39,6 +39,8 @@ __all__ = [
39
39
  "UgcRight",
40
40
  "PhotoData",
41
41
  "SetTopParams",
42
+ "VisitorParams",
43
+ "MessageBoardParams",
42
44
  ]
43
45
 
44
46
 
@@ -320,3 +322,18 @@ class SetTopParams(QzoneRequestParams):
320
322
  @field_serializer("set_top", return_type=str)
321
323
  def serialize_set_top(self, set_top: bool) -> str:
322
324
  return "set" if set_top else "cancel"
325
+
326
+
327
+ class VisitorParams(QzoneRequestParams):
328
+ uin_fields = ("uin",)
329
+ mask: int = 7
330
+ page: int = 1
331
+ fupdate: int = 1
332
+ clear: int = 1
333
+
334
+
335
+ class MessageBoardParams(QzoneRequestParams):
336
+ uin_fields = ("uin",)
337
+ hostUin: int
338
+ num: int = 10
339
+ start: int = 0
@@ -15,6 +15,7 @@ from pydantic import (
15
15
  AliasChoices,
16
16
  AliasPath,
17
17
  BaseModel,
18
+ ConfigDict,
18
19
  Field,
19
20
  HttpUrl,
20
21
  TypeAdapter,
@@ -55,6 +56,14 @@ __all__ = [
55
56
  "PicInfo",
56
57
  "ProfileFeedData",
57
58
  "AvatarResponse",
59
+ "VisitorResp",
60
+ "VisitorUser",
61
+ "VisitorItem",
62
+ "ShuoshuoSnapshot",
63
+ "ReplyItem",
64
+ "CommentListItem",
65
+ "AuthorInfo",
66
+ "MessageBoardResp",
58
67
  ]
59
68
 
60
69
  validate_strdict = TypeAdapter(StrDict).validate_python
@@ -361,3 +370,110 @@ class AvatarResponse(QzoneResponse):
361
370
  @classmethod
362
371
  async def response_to_object(cls, response: ClientResponse) -> "StrDict":
363
372
  return {"avatar": await response.content.read()}
373
+
374
+
375
+ class VisitorUser(BaseModel):
376
+ uin: int
377
+ src: int
378
+ platform_src: int
379
+ service_src: int
380
+ time: int
381
+ hide_from: int
382
+ is_hide_visit: int
383
+ name: str
384
+ yellow: int = -1
385
+ supervip: int = 0
386
+
387
+
388
+ class ShuoshuoSnapshot(BaseModel):
389
+ model_config = ConfigDict(union_mode="left_to_right")
390
+
391
+ id: str
392
+ src: int
393
+ platform_src: int
394
+ service_src: int
395
+ vtime: int
396
+ url: t.Union[HttpUrl, str]
397
+ imgsrc: t.Union[HttpUrl, str]
398
+ name: str
399
+
400
+
401
+ class VisitorItem(BaseModel):
402
+ uin: int
403
+ src: int
404
+ platform_src: int
405
+ service_src: int
406
+ time: int
407
+ hide_from: int
408
+ is_hide_visit: int
409
+ name: str
410
+ yellow: int = -1
411
+ supervip: int = 0
412
+ uins: t.List[VisitorUser] = []
413
+ shuoshuoes: t.List[ShuoshuoSnapshot] = []
414
+
415
+
416
+ class VisitorResp(QzoneResponse):
417
+ Ishost: int = 0
418
+ page: int = 1
419
+ totalpage: int = 1
420
+ todaycount: int = 0
421
+ totalcount: int = 0
422
+ noright_todaycount: int = 0
423
+ noright_totalcount: int = 0
424
+ items: t.List[VisitorItem] = []
425
+
426
+ @classmethod
427
+ async def response_to_object(cls, response: ClientResponse) -> StrDict:
428
+ m = response_callback.search(await response.text())
429
+ if not m:
430
+ raise TryAgain("VisitorResp: callback not found")
431
+ return validate_strdict(json_loads(m.group(1)))
432
+
433
+
434
+ class ReplyItem(BaseModel):
435
+ content: str
436
+ uin: int
437
+ time: int
438
+ nick: str
439
+
440
+
441
+ class CommentListItem(BaseModel):
442
+ id: str
443
+ secret: int
444
+ pasterid: str
445
+ bmp: str
446
+ pubtime: str
447
+ modifytime: int
448
+ effect: int
449
+ type: int
450
+
451
+ uin: t.Optional[int] = None
452
+ nickname: t.Optional[str] = None
453
+ capacity: t.Optional[int] = None
454
+ htmlContent: t.Optional[str] = None
455
+ ubbContent: t.Optional[str] = None
456
+ signature: t.Optional[str] = None
457
+ replyList: t.Optional[t.List[ReplyItem]] = Field(default_factory=list)
458
+
459
+
460
+ class AuthorInfo(BaseModel):
461
+ msg: str = ""
462
+ htmlMsg: str = ""
463
+ sign: str = ""
464
+
465
+
466
+ class MessageBoardResp(QzoneResponse):
467
+ auditNum: t.Optional[int] = None
468
+ auditON: t.Optional[int] = None
469
+
470
+ total: int
471
+ authorInfo: AuthorInfo = Field(default_factory=AuthorInfo)
472
+ commentList: t.List[CommentListItem] = Field(default_factory=list)
473
+
474
+ @classmethod
475
+ async def response_to_object(cls, response: ClientResponse) -> StrDict:
476
+ m = response_callback.search(await response.text())
477
+ if not m:
478
+ raise TryAgain("MessageBoardResp: callback not found")
479
+ return validate_strdict(json_loads(m.group(1)))
@@ -41,10 +41,11 @@ class AstLoader:
41
41
  :return: A :obj:`JsonValue` object.
42
42
  """
43
43
  js = dedent(js).replace(r"\/", "/")
44
- node = ast.parse(js, mode="eval")
44
+ node = ast.parse(js, filename=filename, mode="eval")
45
45
  node = ast.fix_missing_locations(cls.RewriteUndef().visit(node))
46
- code = compile(node, filename, mode="eval")
47
- return eval(code)
46
+ # Parse as a data literal only. Never compile/eval the input: it comes from
47
+ # remote responses and must not be executed as Python code.
48
+ return ast.literal_eval(node)
48
49
 
49
50
 
50
51
  def json_loads(js: str) -> JsonValue:
File without changes