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.
- {pyyapper-1.3.0/src/pyyapper.egg-info → pyyapper-1.3.2}/PKG-INFO +3 -1
- {pyyapper-1.3.0 → pyyapper-1.3.2}/pyproject.toml +2 -1
- pyyapper-1.3.2/src/pyyapper/_version.py +1 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/bot.py +117 -21
- pyyapper-1.3.2/src/pyyapper/cache.py +52 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/core.py +7 -4
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/tasks.py +3 -2
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/http.py +23 -3
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/pagination.py +5 -1
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/wait_for.py +2 -1
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/webhooks.py +28 -4
- {pyyapper-1.3.0 → pyyapper-1.3.2/src/pyyapper.egg-info}/PKG-INFO +3 -1
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/SOURCES.txt +1 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/requires.txt +3 -0
- pyyapper-1.3.0/src/pyyapper/_version.py +0 -1
- {pyyapper-1.3.0 → pyyapper-1.3.2}/LICENSE +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/README.md +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/setup.cfg +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/__init__.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/buttons.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/commands.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/embeds.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/errors.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/__init__.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/__init__.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/checks.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/converters.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/ext/commands/errors.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/models.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/presence.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper/py.typed +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/dependency_links.txt +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/src/pyyapper.egg-info/top_level.txt +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_ext_commands.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_smoke.py +0 -0
- {pyyapper-1.3.0 → pyyapper-1.3.2}/tests/test_tasks.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
loop.
|
|
382
|
-
|
|
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:
|
|
135
|
+
# Fallback: create a new event loop (cross-platform).
|
|
136
136
|
try:
|
|
137
|
-
loop = asyncio.
|
|
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
|
-
|
|
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 =
|
|
115
|
-
except
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
155
|
-
except
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|