pyyapper 1.3.0__tar.gz → 1.3.2__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 (37) hide show
  1. {pyyapper-1.3.0/src/pyyapper.egg-info → pyyapper-1.3.2}/PKG-INFO +3 -1
  2. {pyyapper-1.3.0 → pyyapper-1.3.2}/pyproject.toml +2 -1
  3. pyyapper-1.3.2/src/pyyapper/_version.py +1 -0
  4. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/bot.py +117 -21
  5. pyyapper-1.3.2/src/pyyapper/cache.py +52 -0
  6. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/core.py +7 -4
  7. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/tasks.py +3 -2
  8. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/http.py +23 -3
  9. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/pagination.py +5 -1
  10. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/wait_for.py +2 -1
  11. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/webhooks.py +28 -4
  12. {pyyapper-1.3.0 → pyyapper-1.3.2/src/pyyapper.egg-info}/PKG-INFO +3 -1
  13. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/SOURCES.txt +1 -0
  14. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/requires.txt +3 -0
  15. pyyapper-1.3.0/src/pyyapper/_version.py +0 -1
  16. {pyyapper-1.3.0 → pyyapper-1.3.2}/LICENSE +0 -0
  17. {pyyapper-1.3.0 → pyyapper-1.3.2}/README.md +0 -0
  18. {pyyapper-1.3.0 → pyyapper-1.3.2}/setup.cfg +0 -0
  19. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/__init__.py +0 -0
  20. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/buttons.py +0 -0
  21. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/commands.py +0 -0
  22. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/embeds.py +0 -0
  23. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/errors.py +0 -0
  24. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/__init__.py +0 -0
  25. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/__init__.py +0 -0
  26. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/checks.py +0 -0
  27. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/converters.py +0 -0
  28. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/errors.py +0 -0
  29. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/models.py +0 -0
  30. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/presence.py +0 -0
  31. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/py.typed +0 -0
  32. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/dependency_links.txt +0 -0
  33. {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/top_level.txt +0 -0
  34. {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_ext_commands.py +0 -0
  35. {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_smoke.py +0 -0
  36. {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_tasks.py +0 -0
  37. {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyyapper
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: An intuitive, async Python library for the Yapper bot API.
5
5
  Author: PyYapper contributors
6
6
  License-Expression: MIT
@@ -31,6 +31,8 @@ Requires-Dist: twine; extra == "dev"
31
31
  Provides-Extra: docs
32
32
  Requires-Dist: mkdocs; extra == "docs"
33
33
  Requires-Dist: mkdocs-material; extra == "docs"
34
+ Provides-Extra: speed
35
+ Requires-Dist: orjson; extra == "speed"
34
36
  Dynamic: license-file
35
37
 
36
38
  # PyYapper
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyyapper"
7
- version = "1.3.0"
7
+ version = "1.3.2"
8
8
  description = "An intuitive, async Python library for the Yapper bot API."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -33,6 +33,7 @@ dependencies = [
33
33
  [project.optional-dependencies]
34
34
  dev = ["pytest", "pytest-asyncio", "build", "twine"]
35
35
  docs = ["mkdocs", "mkdocs-material"]
36
+ speed = ["orjson"]
36
37
 
37
38
  [project.urls]
38
39
  Homepage = "https://github.com/pyyapper/pyyapper"
@@ -0,0 +1 @@
1
+ __version__ = "1.3.2"
@@ -17,7 +17,10 @@ from .models import (
17
17
  Role,
18
18
  Server,
19
19
  User,
20
+ VoiceChannel,
20
21
  )
22
+ from .cache import TTLCache, MISSING as _CACHE_MISSING
23
+ from .presence import Activity, Presence
21
24
  from .embeds import Embed
22
25
  from .buttons import Button, Row
23
26
 
@@ -177,6 +180,7 @@ class Bot:
177
180
  log: bool = True,
178
181
  ignore_bots: bool = True,
179
182
  max_age: Optional[int] = None,
183
+ cache_ttl: float = 0.0,
180
184
  ) -> None:
181
185
  if token is None:
182
186
  import os
@@ -186,6 +190,8 @@ class Bot:
186
190
  self.ignore_bots = bool(ignore_bots)
187
191
  self.max_age = max_age
188
192
  self._presence = None
193
+ self._cache = TTLCache(default_ttl=cache_ttl)
194
+ self.cache_ttl = cache_ttl
189
195
  self.prefixes = prefixes if prefixes is not None else ["/", "!"]
190
196
  self.log = log
191
197
 
@@ -438,27 +444,71 @@ class Bot:
438
444
  # ------------------------------------------------------------------ #
439
445
  # Servers / channels / members
440
446
  # ------------------------------------------------------------------ #
441
- async def servers(self) -> list[Server]:
447
+ async def servers(self, *, cache_ttl: Optional[float] = None, force: bool = False) -> list[Server]:
448
+ key = ("servers",)
449
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
450
+ if not force and _eff_ttl and _eff_ttl > 0:
451
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
452
+ cached = self._cache.get(key)
453
+ if cached is not _CACHE_MISSING:
454
+ return cached
442
455
  out = await self.api("GET", "/servers")
443
- return [Server.from_dict(s) for s in (out.get("servers") or [])]
444
-
445
- async def channels(self, server_id: Any = None, *, type: Optional[str] = None) -> list[Channel]:
456
+ res = [Server.from_dict(s) for s in (out.get("servers") or [])]
457
+ if cache_ttl != 0:
458
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
459
+ self._cache.set(key, res, ttl)
460
+ return res
461
+
462
+ async def channels(self, server_id: Any = None, *, type: Optional[str] = None, cache_ttl: Optional[float] = None, force: bool = False) -> list[Channel]:
463
+ key = ("channels", server_id, type)
464
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
465
+ if not force and _eff_ttl and _eff_ttl > 0:
466
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
467
+ cached = self._cache.get(key)
468
+ if cached is not _CACHE_MISSING:
469
+ return cached
446
470
  q = f"?type={type}" if type else ""
447
471
  path = f"/servers/{server_id}/channels{q}" if server_id else f"/channels{q}"
448
472
  out = await self.api("GET", path)
449
- return [Channel.from_dict(c) for c in (out.get("channels") or [])]
450
-
451
- async def categories(self, server_id: Any) -> list[Category]:
473
+ res = [Channel.from_dict(c) for c in (out.get("channels") or [])]
474
+ if cache_ttl != 0:
475
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
476
+ self._cache.set(key, res, ttl)
477
+ return res
478
+
479
+ async def categories(self, server_id: Any, *, cache_ttl: Optional[float] = None, force: bool = False) -> list[Category]:
480
+ key = ("categories", server_id)
481
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
482
+ if not force and _eff_ttl and _eff_ttl > 0:
483
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
484
+ cached = self._cache.get(key)
485
+ if cached is not _CACHE_MISSING:
486
+ return cached
452
487
  out = await self.api("GET", f"/servers/{server_id}/channels")
453
- return [Category.from_dict(c) for c in (out.get("categories") or [])]
454
-
455
- async def channel(self, channel_id: Any) -> Optional[Channel]:
488
+ res = [Category.from_dict(c) for c in (out.get("categories") or [])]
489
+ if cache_ttl != 0:
490
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
491
+ self._cache.set(key, res, ttl)
492
+ return res
493
+
494
+ async def channel(self, channel_id: Any, *, cache_ttl: Optional[float] = None, force: bool = False) -> Optional[Channel]:
495
+ key = ("channel", channel_id)
496
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
497
+ if not force and _eff_ttl and _eff_ttl > 0:
498
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
499
+ cached = self._cache.get(key)
500
+ if cached is not _CACHE_MISSING:
501
+ return cached
456
502
  try:
457
503
  out = await self.api("GET", f"/channels/{channel_id}")
458
504
  except Exception:
459
505
  return None
460
506
  c = out.get("channel") if isinstance(out, dict) else None
461
- return Channel.from_dict(c) if c else None
507
+ res = Channel.from_dict(c) if c else None
508
+ if cache_ttl != 0:
509
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
510
+ self._cache.set(key, res, ttl)
511
+ return res
462
512
 
463
513
  async def members(self, server_id: Any, after: Any = None) -> list[Member]:
464
514
  path = f"/servers/{server_id}/members?limit=200"
@@ -471,10 +521,21 @@ class Bot:
471
521
  out = await self.api("GET", f"/channels/{channel_id}/messages?limit={limit}")
472
522
  return [Message.from_dict(m) for m in (out.get("messages") or [])]
473
523
 
474
- async def user(self, user_id: Any) -> User:
524
+ async def user(self, user_id: Any, *, cache_ttl: Optional[float] = None, force: bool = False) -> User:
525
+ key = ("user", user_id)
526
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
527
+ if not force and _eff_ttl and _eff_ttl > 0:
528
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
529
+ cached = self._cache.get(key)
530
+ if cached is not _CACHE_MISSING:
531
+ return cached
475
532
  out = await self.api("GET", f"/users/{user_id}")
476
533
  u = out.get("user") if isinstance(out, dict) else out
477
- return User.from_dict(u)
534
+ res = User.from_dict(u)
535
+ if cache_ttl != 0:
536
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
537
+ self._cache.set(key, res, ttl)
538
+ return res
478
539
 
479
540
  # ------------------------------------------------------------------ #
480
541
  # Profile
@@ -494,7 +555,6 @@ class Bot:
494
555
  return await self.http.upload("PUT", "/me/banner", image, type)
495
556
 
496
557
  async def set_status(self, status: str = "online", activity: Any = None) -> Any:
497
- from .presence import Presence, Activity
498
558
  if isinstance(activity, Activity):
499
559
  activity = activity.to_text()
500
560
  elif isinstance(activity, Presence):
@@ -503,7 +563,6 @@ class Bot:
503
563
  return await self.api("PUT", "/me/status", {"status": status, "activity": activity})
504
564
 
505
565
  async def set_presence(self, *, status: str = "online", activity: Any = None) -> Any:
506
- from .presence import Presence
507
566
  self._presence = Presence(status=status, activity=activity)
508
567
  return await self.set_status(status=status, activity=activity)
509
568
 
@@ -567,13 +626,35 @@ class Bot:
567
626
  # ------------------------------------------------------------------ #
568
627
  # Moderation
569
628
  # ------------------------------------------------------------------ #
570
- async def server(self, server_id: Any) -> Any:
629
+ async def server(self, server_id: Any, *, cache_ttl: Optional[float] = None, force: bool = False) -> Any:
630
+ key = ("server", server_id)
631
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
632
+ if not force and _eff_ttl and _eff_ttl > 0:
633
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
634
+ cached = self._cache.get(key)
635
+ if cached is not _CACHE_MISSING:
636
+ return cached
571
637
  out = await self.api("GET", f"/servers/{server_id}")
572
- return out.get("server") if isinstance(out, dict) else out
573
-
574
- async def roles(self, server_id: Any) -> list[Role]:
638
+ res = out.get("server") if isinstance(out, dict) else out
639
+ if cache_ttl != 0:
640
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
641
+ self._cache.set(key, res, ttl)
642
+ return res
643
+
644
+ async def roles(self, server_id: Any, *, cache_ttl: Optional[float] = None, force: bool = False) -> list[Role]:
645
+ key = ("roles", server_id)
646
+ _eff_ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
647
+ if not force and _eff_ttl and _eff_ttl > 0:
648
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
649
+ cached = self._cache.get(key)
650
+ if cached is not _CACHE_MISSING:
651
+ return cached
575
652
  out = await self.api("GET", f"/servers/{server_id}/roles")
576
- return [Role.from_dict(r) for r in (out.get("roles") or [])]
653
+ res = [Role.from_dict(r) for r in (out.get("roles") or [])]
654
+ if cache_ttl != 0:
655
+ ttl = self._cache.default_ttl if cache_ttl is None else cache_ttl
656
+ self._cache.set(key, res, ttl)
657
+ return res
577
658
 
578
659
  async def member(self, server_id: Any, user_id: Any) -> Member:
579
660
  out = await self.api("GET", f"/servers/{server_id}/members/{user_id}")
@@ -611,7 +692,6 @@ class Bot:
611
692
  # Voice
612
693
  # ------------------------------------------------------------------ #
613
694
  async def voice(self, server_id: Any) -> list:
614
- from .models import VoiceChannel
615
695
  out = await self.api("GET", f"/servers/{server_id}/voice")
616
696
  voice = out.get("voice", []) if isinstance(out, dict) else []
617
697
  return [VoiceChannel.from_dict(v) for v in (voice or [])]
@@ -798,6 +878,19 @@ class Bot:
798
878
  def stop(self) -> None:
799
879
  self.running = False
800
880
 
881
+ def invalidate_cache(self, key: Any = None) -> None:
882
+ """Drop cached read results. ``key=None`` clears everything."""
883
+ self._cache.invalidate(key)
884
+
885
+ async def send_many(self, channel_id: Any, contents: Any, *, concurrency: int = 5, **opts: Any) -> list:
886
+ sem = asyncio.Semaphore(max(1, int(concurrency)))
887
+
888
+ async def _one(c: Any) -> Any:
889
+ async with sem:
890
+ return await self.send(channel_id, c, **opts)
891
+
892
+ return list(await asyncio.gather(*(_one(c) for c in (contents or [])), return_exceptions=True))
893
+
801
894
  # ------------------------------------------------------------------ #
802
895
  # Internals
803
896
  # ------------------------------------------------------------------ #
@@ -818,6 +911,9 @@ class Bot:
818
911
  except Exception:
819
912
  pass
820
913
 
914
+ if len(fns) == 1:
915
+ await _run(fns[0])
916
+ return
821
917
  # Dispatch handlers concurrently to reduce latency when several
822
918
  # listeners are registered for the same event. Order is not guaranteed.
823
919
  await asyncio.gather(*(_run(fn) for fn in fns), return_exceptions=True)
@@ -0,0 +1,52 @@
1
+ """Tiny TTL cache used by PyYapper for read-heavy API calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Optional
7
+
8
+ _MISSING = object()
9
+
10
+
11
+ class TTLCache:
12
+ """A simple in-memory TTL cache.
13
+
14
+ Keys may be any hashable value. ``get`` returns the sentinel
15
+ :data:`MISSING` when a key is absent or expired.
16
+ """
17
+
18
+ def __init__(self, default_ttl: float = 15.0) -> None:
19
+ self._store: dict[Any, tuple[Any, float]] = {}
20
+ self.default_ttl = float(default_ttl)
21
+
22
+ def get(self, key: Any) -> Any:
23
+ entry = self._store.get(key)
24
+ if entry is None:
25
+ return _MISSING
26
+ value, expiry = entry
27
+ if time.monotonic() >= expiry:
28
+ self._store.pop(key, None)
29
+ return _MISSING
30
+ return value
31
+
32
+ def set(self, key: Any, value: Any, ttl: Optional[float] = None) -> None:
33
+ if ttl is None:
34
+ ttl = self.default_ttl
35
+ if ttl and ttl > 0:
36
+ self._store[key] = (value, time.monotonic() + float(ttl))
37
+
38
+ def invalidate(self, key: Any = None) -> None:
39
+ if key is None:
40
+ self._store.clear()
41
+ else:
42
+ self._store.pop(key, None)
43
+
44
+ def invalidate_prefix(self, prefix: Any) -> None:
45
+ for k in [k for k in list(self._store.keys()) if isinstance(k, tuple) and k and k[0] == prefix]:
46
+ self._store.pop(k, None)
47
+
48
+ def size(self) -> int:
49
+ return len(self._store)
50
+
51
+
52
+ MISSING = _MISSING
@@ -376,10 +376,13 @@ def remove_cog(bot, name):
376
376
  import asyncio
377
377
  # best-effort: if event loop running, schedule; else run
378
378
  try:
379
- loop = asyncio.get_event_loop()
380
- if loop.is_running():
381
- loop.create_task(res)
382
- else:
379
+ try:
380
+ loop = asyncio.get_running_loop()
381
+ if loop.is_running():
382
+ loop.create_task(res)
383
+ except RuntimeError:
384
+ loop = asyncio.new_event_loop()
385
+ asyncio.set_event_loop(loop)
383
386
  loop.run_until_complete(res)
384
387
  except Exception:
385
388
  pass
@@ -132,9 +132,10 @@ class Loop:
132
132
  try:
133
133
  loop = asyncio.get_running_loop()
134
134
  except RuntimeError:
135
- # Fallback: try to get/create an event loop.
135
+ # Fallback: create a new event loop (cross-platform).
136
136
  try:
137
- loop = asyncio.get_event_loop()
137
+ loop = asyncio.new_event_loop()
138
+ asyncio.set_event_loop(loop)
138
139
  except RuntimeError:
139
140
  return None
140
141
  try:
@@ -15,6 +15,25 @@ from __future__ import annotations
15
15
  import asyncio
16
16
  import json
17
17
  import random
18
+
19
+ try:
20
+ import orjson as _orjson
21
+
22
+ def _json_loads(data):
23
+ return _orjson.loads(data)
24
+
25
+ def _json_dumps(obj):
26
+ return _orjson.dumps(obj)
27
+
28
+ _HAS_ORJSON = True
29
+ except ImportError: # pragma: no cover
30
+ _HAS_ORJSON = False
31
+
32
+ def _json_loads(data):
33
+ return json.loads(data)
34
+
35
+ def _json_dumps(obj):
36
+ return json.dumps(obj).encode()
18
37
  from typing import Any, Optional
19
38
 
20
39
  import aiohttp
@@ -93,7 +112,8 @@ class HTTPClient:
93
112
  # JSON body unless caller already provided bytes
94
113
  if content_type is None:
95
114
  content_type = "application/json"
96
- data = json.dumps(body)
115
+ dumped = _json_dumps(body)
116
+ data = dumped if isinstance(dumped, (bytes, bytearray)) else dumped.encode()
97
117
  elif body is not None:
98
118
  data = body
99
119
  if content_type:
@@ -111,8 +131,8 @@ class HTTPClient:
111
131
  raw = await res.read()
112
132
  text = raw.decode("utf-8", errors="replace") if raw else ""
113
133
  try:
114
- payload = json.loads(text) if text else {}
115
- except json.JSONDecodeError:
134
+ payload = _json_loads(text) if text else {}
135
+ except Exception:
116
136
  payload = {}
117
137
  if res.status < 400:
118
138
  return payload
@@ -62,7 +62,11 @@ class Paginator:
62
62
  return [Row(*btns)]
63
63
 
64
64
  async def _wait(self) -> Any:
65
- loop = asyncio.get_event_loop()
65
+ try:
66
+ loop = asyncio.get_running_loop()
67
+ except RuntimeError:
68
+ loop = asyncio.new_event_loop()
69
+ asyncio.set_event_loop(loop)
66
70
  self._future = loop.create_future()
67
71
  try:
68
72
  return await asyncio.wait_for(self._future, timeout=self.timeout)
@@ -104,7 +104,8 @@ class WaiterRegistry:
104
104
  try:
105
105
  loop = asyncio.get_running_loop()
106
106
  except RuntimeError:
107
- loop = asyncio.get_event_loop()
107
+ loop = asyncio.new_event_loop()
108
+ asyncio.set_event_loop(loop)
108
109
  future = loop.create_future()
109
110
  waiter = self.add(event, check, future)
110
111
  try:
@@ -10,6 +10,25 @@ from __future__ import annotations
10
10
  import asyncio
11
11
  import json
12
12
  from typing import Any, Optional
13
+
14
+ try:
15
+ import orjson as _orjson
16
+
17
+ def _json_loads(data):
18
+ return _orjson.loads(data)
19
+
20
+ def _json_dumps(obj):
21
+ return _orjson.dumps(obj)
22
+
23
+ _HAS_ORJSON = True
24
+ except ImportError: # pragma: no cover
25
+ _HAS_ORJSON = False
26
+
27
+ def _json_loads(data):
28
+ return json.loads(data)
29
+
30
+ def _json_dumps(obj):
31
+ return json.dumps(obj).encode()
13
32
  from urllib.parse import urlparse
14
33
 
15
34
  import aiohttp
@@ -134,11 +153,16 @@ class Webhook:
134
153
  session = await self._get_session()
135
154
  for attempt in range(retries + 1):
136
155
  try:
156
+ if json_body is None:
157
+ data_body = None
158
+ else:
159
+ dumped = _json_dumps(json_body)
160
+ data_body = dumped if isinstance(dumped, (bytes, bytearray)) else dumped.encode()
137
161
  kwargs = dict(
138
162
  params=params,
139
- json=json_body,
163
+ data=data_body,
140
164
  timeout=aiohttp.ClientTimeout(total=timeout),
141
- headers={"User-Agent": USER_AGENT},
165
+ headers={"User-Agent": USER_AGENT, "Content-Type": "application/json"} if data_body is not None else {"User-Agent": USER_AGENT},
142
166
  )
143
167
  if hasattr(session, "request"):
144
168
  cm = session.request(method, url, **kwargs)
@@ -151,8 +175,8 @@ class Webhook:
151
175
  raw = await res.read()
152
176
  text = raw.decode("utf-8", errors="replace") if raw else ""
153
177
  try:
154
- payload = json.loads(text) if text else None
155
- except json.JSONDecodeError:
178
+ payload = _json_loads(text) if text else None
179
+ except Exception:
156
180
  payload = None
157
181
  if res.status < 400:
158
182
  return payload
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyyapper
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: An intuitive, async Python library for the Yapper bot API.
5
5
  Author: PyYapper contributors
6
6
  License-Expression: MIT
@@ -31,6 +31,8 @@ Requires-Dist: twine; extra == "dev"
31
31
  Provides-Extra: docs
32
32
  Requires-Dist: mkdocs; extra == "docs"
33
33
  Requires-Dist: mkdocs-material; extra == "docs"
34
+ Provides-Extra: speed
35
+ Requires-Dist: orjson; extra == "speed"
34
36
  Dynamic: license-file
35
37
 
36
38
  # PyYapper
@@ -5,6 +5,7 @@ src/pyyapper/__init__.py
5
5
  src/pyyapper/_version.py
6
6
  src/pyyapper/bot.py
7
7
  src/pyyapper/buttons.py
8
+ src/pyyapper/cache.py
8
9
  src/pyyapper/commands.py
9
10
  src/pyyapper/embeds.py
10
11
  src/pyyapper/errors.py
@@ -9,3 +9,6 @@ twine
9
9
  [docs]
10
10
  mkdocs
11
11
  mkdocs-material
12
+
13
+ [speed]
14
+ orjson
@@ -1 +0,0 @@
1
- __version__ = "1.3.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes