hikari-risa 1.0.0__py3-none-any.whl
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.
- hikari_risa-1.0.0.dist-info/METADATA +83 -0
- hikari_risa-1.0.0.dist-info/RECORD +43 -0
- hikari_risa-1.0.0.dist-info/WHEEL +4 -0
- hikari_risa-1.0.0.dist-info/entry_points.txt +2 -0
- hikari_risa-1.0.0.dist-info/licenses/LICENSE +21 -0
- risa/__init__.py +49 -0
- risa/__main__.py +145 -0
- risa/_about.py +31 -0
- risa/binding.py +251 -0
- risa/client.py +657 -0
- risa/codecs.py +204 -0
- risa/context.py +693 -0
- risa/di.py +75 -0
- risa/dispatch.py +828 -0
- risa/errors.py +603 -0
- risa/internal/__init__.py +22 -0
- risa/internal/codec.py +547 -0
- risa/internal/constants.py +36 -0
- risa/internal/leases.py +197 -0
- risa/internal/locks.py +87 -0
- risa/internal/reader.py +88 -0
- risa/internal/registry.py +208 -0
- risa/internal/wire.py +118 -0
- risa/py.typed +0 -0
- risa/state/__init__.py +25 -0
- risa/state/anchor.py +279 -0
- risa/state/backend.py +155 -0
- risa/state/message.py +278 -0
- risa/state/placement.py +104 -0
- risa/state/schema.py +447 -0
- risa/state/serde.py +69 -0
- risa/state/stateless.py +94 -0
- risa/state/stored.py +444 -0
- risa/store/__init__.py +30 -0
- risa/store/memory.py +152 -0
- risa/store/postgres.py +311 -0
- risa/store/protocol.py +172 -0
- risa/store/redis.py +254 -0
- risa/store/testing.py +341 -0
- risa/ui/__init__.py +26 -0
- risa/ui/build.py +353 -0
- risa/ui/nodes.py +918 -0
- risa/view.py +466 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hikari-risa
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A component handler for hikari with first-class Components V2 support.
|
|
5
|
+
Project-URL: Documentation, https://rayakame.github.io/hikari-risa/
|
|
6
|
+
Project-URL: Repository, https://github.com/rayakame/hikari-risa
|
|
7
|
+
Project-URL: Issues, https://github.com/rayakame/hikari-risa/issues
|
|
8
|
+
Project-URL: Changelog, https://rayakame.github.io/hikari-risa/stable/changelog/
|
|
9
|
+
Author-email: Rayakame <github.android206@passmail.net>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: components,discord,hikari,ui
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
|
+
Classifier: Topic :: Communications :: Chat
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: <3.16,>=3.12
|
|
24
|
+
Requires-Dist: hikari~=2.5
|
|
25
|
+
Requires-Dist: linkd~=0.6
|
|
26
|
+
Requires-Dist: msgspec~=0.22
|
|
27
|
+
Provides-Extra: postgres
|
|
28
|
+
Requires-Dist: asyncpg~=0.32; extra == 'postgres'
|
|
29
|
+
Provides-Extra: radixly
|
|
30
|
+
Requires-Dist: radixly~=1.0; extra == 'radixly'
|
|
31
|
+
Provides-Extra: redis
|
|
32
|
+
Requires-Dist: redis~=8.0; extra == 'redis'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/rayakame/hikari-risa/a14722c65c6df514d463402cde7f7f48d133bd66/assets/web/banner.webp" alt="hikari-risa" width="100%">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
# Components V2 views for hikari, with state that survives restarts.
|
|
40
|
+
|
|
41
|
+
risa is a component handler for [hikari](https://github.com/hikari-py/hikari), built for Components V2 first.
|
|
42
|
+
A view keeps its state in the message it renders, or in a store you choose, so a click still lands where it should
|
|
43
|
+
after a restart.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import risa
|
|
47
|
+
from risa import ui
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@risa.register(name="counter")
|
|
51
|
+
class Counter(risa.View):
|
|
52
|
+
count: int = 0
|
|
53
|
+
|
|
54
|
+
def render(self) -> ui.Layout:
|
|
55
|
+
return ui.Container(
|
|
56
|
+
ui.TextDisplay(f"Clicks: **{self.count}**"),
|
|
57
|
+
ui.Row(ui.Button(self.click, label="+1")),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@risa.handler
|
|
61
|
+
async def click(self, ctx: risa.ComponentContext) -> None:
|
|
62
|
+
self.count += 1
|
|
63
|
+
await ctx.rerender()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
risa runs on Python 3.12 to 3.15.
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
uv add hikari-risa
|
|
72
|
+
pip install hikari-risa
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Extras: `hikari-risa[redis]`, `hikari-risa[postgres]` and `hikari-risa[radixly]`.
|
|
76
|
+
|
|
77
|
+
## Documentation
|
|
78
|
+
|
|
79
|
+
<https://rayakame.github.io/hikari-risa/>
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT, see [LICENSE](https://github.com/rayakame/hikari-risa/blob/main/LICENSE).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
risa/__init__.py,sha256=a_NJtXda3ZLJJbtHo39rDJQ5Q_06PGQpHKPt9M4xFk0,2186
|
|
2
|
+
risa/__main__.py,sha256=HnnV0xAZNA71f4PC75PMMr68MxRVKrubKxp-W9tqoz0,6143
|
|
3
|
+
risa/_about.py,sha256=3lExK5HHXAyS6krv0zr5Qoci_tMBq_OvE3zIE-x4iIM,1501
|
|
4
|
+
risa/binding.py,sha256=BYIR-RFvNQziDNo3xU2V8aKTMkL3wfvzyp7BUFzEV14,8521
|
|
5
|
+
risa/client.py,sha256=DXTDHSrvVmuHPGYJEszqjzYlnRtvRDZDid8291pfmFU,24445
|
|
6
|
+
risa/codecs.py,sha256=trAs37LBMQuR3G-z_ftT2ZE1qZqatV5QaNMa2bQRW6o,6101
|
|
7
|
+
risa/context.py,sha256=17tcr4ONxonlqGCHpS8traT4YccruzjuGT7EQcGpb2A,28047
|
|
8
|
+
risa/di.py,sha256=VX0Y2-UeHj8VRC2SluonqA_z1EIJ962GUv3lhxH2IdM,2619
|
|
9
|
+
risa/dispatch.py,sha256=PsANIZo3Q0FTXZHjpf58Vcd4I23-hQyqQj-sCMShZLQ,30972
|
|
10
|
+
risa/errors.py,sha256=R4FEgn73IPrZqALadAjb0yvCSwyyoafhJsOn6R5fqqY,19663
|
|
11
|
+
risa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
risa/view.py,sha256=Q6Zx3k7QQG1Mq1knTuV03aTphWl4chSN8kM3GCfX7LY,17744
|
|
13
|
+
risa/internal/__init__.py,sha256=UzJ8I-OX6MBnFu0O_yjWVj-FeRu3_n4oaC5GIEe0r50,1224
|
|
14
|
+
risa/internal/codec.py,sha256=EY9WRQLapSeHSv4at1CC8ywchKUkMylcRo0dqZSFkyg,17933
|
|
15
|
+
risa/internal/constants.py,sha256=yMs8QtYmoQ6v_WluENl58UizVxhztdshw7A7_5bVL_w,1520
|
|
16
|
+
risa/internal/leases.py,sha256=ydwbtT9Vqkzm5_9zwRa5LEqndvWP1dT66tsQJbXyXHc,7619
|
|
17
|
+
risa/internal/locks.py,sha256=AfRwaW8pg3dmlnWhK6D8yQKg2S4NKYIO4y_3p_noSbc,2837
|
|
18
|
+
risa/internal/reader.py,sha256=rsY5CwF5PnnM-_6ftLoLY06gx_AyT-WN4nivU0nhQOI,3474
|
|
19
|
+
risa/internal/registry.py,sha256=krNzz3gOKay-7t1Hi4fr4h5npQBLFDHyfIb0kSoWyMw,6230
|
|
20
|
+
risa/internal/wire.py,sha256=PmqrTCa4Q3kuUcAfCa12176X90A46oFYALXu3CF94PM,4008
|
|
21
|
+
risa/state/__init__.py,sha256=gkkfrvJ_m7JqA-jz-C3YhYyCz7sPobIUzt8YLfZHUZU,1260
|
|
22
|
+
risa/state/anchor.py,sha256=aThCV5XvnJju_XS7O2tYDCdIbBD-wR5ovM_aMhIkD34,8999
|
|
23
|
+
risa/state/backend.py,sha256=F8A7MIpiuK1gWzyv3FtBeiVzpi4RC65MXv76Xjyn8FE,4990
|
|
24
|
+
risa/state/message.py,sha256=qZIS4UTDJIO3q1xpadPGEwlThmaTw6wtsgTbRN17x-o,10955
|
|
25
|
+
risa/state/placement.py,sha256=GZblfWfqtAPDQMMF2DyGHikwbD-uHbIIRmN6I8sVNZk,4274
|
|
26
|
+
risa/state/schema.py,sha256=8qQzUl-sV9JC-x16VVYcpvhzowHkHMYjXIRIT4Aa2_c,15607
|
|
27
|
+
risa/state/serde.py,sha256=v1TMQpqlGZ2YXvavn-up7W2haqNb6J2-u303U3j-b4s,2118
|
|
28
|
+
risa/state/stateless.py,sha256=DZA6X9kdRZa7rE255RTJHQ-e5utUyoZ24TkaySxg4Pg,2834
|
|
29
|
+
risa/state/stored.py,sha256=Wcqnf1bVhEcv2S9jE1fXUH0E0_hG_T9rVuvpdB5RKGk,16554
|
|
30
|
+
risa/store/__init__.py,sha256=9_ttAl496pFHT2YFmBIRMyU8YwId-VqnTp1ENe1NKoQ,1460
|
|
31
|
+
risa/store/memory.py,sha256=3Nw0t7BW_NH485_tOyklzMQvg_4GBxqjp7MJ3OSNlJw,5684
|
|
32
|
+
risa/store/postgres.py,sha256=gjKny5MidGcOZWmb-wK3EaiCUK5LLb23wIXzAt9kFaY,11326
|
|
33
|
+
risa/store/protocol.py,sha256=8nSNsbnqlaNLr5QpGWb3kP9A4WT9jkfdbBlmklQVoY8,6355
|
|
34
|
+
risa/store/redis.py,sha256=4-AzdgCHxZqHJTzaxESxMaQZTyUdHNifbo0AqBLRJ7A,8883
|
|
35
|
+
risa/store/testing.py,sha256=ssjhl6hyeDNjuZ8L0-zu-ryE-v7WAxERweTBc-HAPsQ,14779
|
|
36
|
+
risa/ui/__init__.py,sha256=eHlB_84IOAy-GYQG_tzMrUb1EGpFAWprX_hElTB5WoU,1336
|
|
37
|
+
risa/ui/build.py,sha256=iAWjxbP73OZFcdskNak5v7MjhVCGyP4WeoKa1dXe8H0,12702
|
|
38
|
+
risa/ui/nodes.py,sha256=p7lpOkIgMvbPCcdx4Vtzh1QW42oJ6nsnB-_kpcCpoUc,26489
|
|
39
|
+
hikari_risa-1.0.0.dist-info/METADATA,sha256=dy_PD3ydXjyUaVo332XOXE-Qnw5A1J9jwOubSaxqBUg,2665
|
|
40
|
+
hikari_risa-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
41
|
+
hikari_risa-1.0.0.dist-info/entry_points.txt,sha256=w3ft47wSzuclUu3sHhVWFSUBp7FVdfJzNPYTPh2wH3Q,51
|
|
42
|
+
hikari_risa-1.0.0.dist-info/licenses/LICENSE,sha256=7jFRgI-fZKWKHnewHc6a6uSu3_nvAQ0TzMMoJOLdEeE,1060
|
|
43
|
+
hikari_risa-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ray
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
risa/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (c) 2025 Rayakame
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
# copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""A component handler for hikari, built on Discord's Components V2.
|
|
21
|
+
|
|
22
|
+
A view is a class whose fields are its state, whose `render` returns its layout, and
|
|
23
|
+
whose handlers answer the clicks on it. The state travels inside the message by
|
|
24
|
+
default, so views survive restarts with no infrastructure. A view that outgrows the
|
|
25
|
+
message can keep its state in a store.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from risa import codecs as codecs
|
|
31
|
+
from risa import di as di
|
|
32
|
+
from risa import errors as errors
|
|
33
|
+
from risa import state as state
|
|
34
|
+
from risa import store as store
|
|
35
|
+
from risa import ui as ui
|
|
36
|
+
from risa._about import __author__ as __author__
|
|
37
|
+
from risa._about import __copyright__ as __copyright__
|
|
38
|
+
from risa._about import __discord_invite__ as __discord_invite__
|
|
39
|
+
from risa._about import __license__ as __license__
|
|
40
|
+
from risa._about import __url__ as __url__
|
|
41
|
+
from risa._about import __version__ as __version__
|
|
42
|
+
from risa.binding import *
|
|
43
|
+
from risa.client import *
|
|
44
|
+
from risa.context import *
|
|
45
|
+
from risa.di import *
|
|
46
|
+
from risa.errors import *
|
|
47
|
+
from risa.state import *
|
|
48
|
+
from risa.store import *
|
|
49
|
+
from risa.view import *
|
risa/__main__.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright (c) 2025 Rayakame
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
# copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""The ``hikari-risa`` command, also ``python -m risa``: the versions a bug report needs."""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import ctypes
|
|
26
|
+
import importlib.metadata
|
|
27
|
+
import os
|
|
28
|
+
import pathlib
|
|
29
|
+
import platform
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
import typing
|
|
33
|
+
|
|
34
|
+
import risa
|
|
35
|
+
|
|
36
|
+
if typing.TYPE_CHECKING:
|
|
37
|
+
import collections.abc
|
|
38
|
+
|
|
39
|
+
__all__ = ("main",)
|
|
40
|
+
|
|
41
|
+
_ART: typing.Final[collections.abc.Sequence[str]] = (
|
|
42
|
+
r" _ ",
|
|
43
|
+
r" _____(_)________ _",
|
|
44
|
+
r" / ___/ / ___/ __ `/",
|
|
45
|
+
r" / / / (__ ) /_/ / ",
|
|
46
|
+
r"/_/ /_/____/\__,_/ ",
|
|
47
|
+
)
|
|
48
|
+
# 256-colour codes that read on dark and light terminals.
|
|
49
|
+
_ART_COLOURS: typing.Final[collections.abc.Sequence[str]] = ("38;5;205", "38;5;206", "38;5;170", "38;5;134", "38;5;98")
|
|
50
|
+
_PINK: typing.Final[str] = "38;5;205"
|
|
51
|
+
_BOLD: typing.Final[str] = "1"
|
|
52
|
+
_DIM: typing.Final[str] = "2"
|
|
53
|
+
_LABEL_WIDTH: typing.Final[int] = 10
|
|
54
|
+
_EXTRA: typing.Final[re.Pattern[str]] = re.compile(r"""extra\s*==\s*["']([^"']+)["']""")
|
|
55
|
+
_STD_OUTPUT_HANDLE: typing.Final[int] = -11
|
|
56
|
+
_ENABLE_VIRTUAL_TERMINAL_PROCESSING: typing.Final[int] = 0x0004
|
|
57
|
+
|
|
58
|
+
type _Paint = collections.abc.Callable[[str, str], str]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _escape_codes_work() -> bool:
|
|
62
|
+
# A Windows console reads escape codes only once the program turns on virtual terminal processing.
|
|
63
|
+
if sys.platform != "win32":
|
|
64
|
+
return True
|
|
65
|
+
kernel32 = ctypes.WinDLL("kernel32")
|
|
66
|
+
kernel32.GetStdHandle.restype = ctypes.c_void_p
|
|
67
|
+
kernel32.GetConsoleMode.argtypes = (ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32))
|
|
68
|
+
kernel32.SetConsoleMode.argtypes = (ctypes.c_void_p, ctypes.c_uint32)
|
|
69
|
+
handle = kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
|
|
70
|
+
mode = ctypes.c_uint32()
|
|
71
|
+
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
72
|
+
return False
|
|
73
|
+
return bool(kernel32.SetConsoleMode(handle, mode.value | _ENABLE_VIRTUAL_TERMINAL_PROCESSING))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _section(
|
|
77
|
+
paint: _Paint, label: str, rows: collections.abc.Sequence[collections.abc.Sequence[str]]
|
|
78
|
+
) -> collections.abc.Iterator[str]:
|
|
79
|
+
# The last column goes unpadded, so it may carry colour codes without breaking the alignment.
|
|
80
|
+
widths = [max(len(row[column]) for row in rows) for column in range(len(rows[0]) - 1)] if rows else []
|
|
81
|
+
for index, row in enumerate(rows):
|
|
82
|
+
cells = [cell.ljust(width) for cell, width in zip(row, widths, strict=False)]
|
|
83
|
+
heading = paint((label if index == 0 else "").ljust(_LABEL_WIDTH), _PINK)
|
|
84
|
+
yield f" {heading}{' '.join([*cells, row[-1]])}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _report(*, colour: bool) -> collections.abc.Iterator[str]:
|
|
88
|
+
def paint(text: str, style: str) -> str:
|
|
89
|
+
return f"\x1b[{style}m{text}\x1b[0m" if colour else text
|
|
90
|
+
|
|
91
|
+
for line, style in zip(_ART[:-1], _ART_COLOURS, strict=False):
|
|
92
|
+
yield paint(line.rstrip(), style)
|
|
93
|
+
yield paint(_ART[-1], _ART_COLOURS[-1]) + paint(f"hikari-risa {risa.__version__}", _BOLD)
|
|
94
|
+
|
|
95
|
+
metadata = importlib.metadata.metadata("hikari-risa")
|
|
96
|
+
urls = dict(url.split(", ", 1) for url in metadata.get_all("Project-URL") or ())
|
|
97
|
+
required: list[collections.abc.Sequence[str]] = []
|
|
98
|
+
extras: list[collections.abc.Sequence[str]] = []
|
|
99
|
+
for requirement in importlib.metadata.requires("hikari-risa") or ():
|
|
100
|
+
name = re.split(r"[^\w.-]", requirement, maxsplit=1)[0]
|
|
101
|
+
try:
|
|
102
|
+
version = importlib.metadata.version(name)
|
|
103
|
+
except importlib.metadata.PackageNotFoundError:
|
|
104
|
+
version = paint("not installed", _DIM)
|
|
105
|
+
if extra := _EXTRA.search(requirement):
|
|
106
|
+
extras.append((extra[1], name, version))
|
|
107
|
+
else:
|
|
108
|
+
required.append((name, version))
|
|
109
|
+
|
|
110
|
+
yield ""
|
|
111
|
+
yield paint(metadata["Summary"], _DIM)
|
|
112
|
+
yield ""
|
|
113
|
+
yield from _section(paint, "python", [(f"{platform.python_implementation()} {platform.python_version()}",)])
|
|
114
|
+
yield from _section(paint, "platform", [(platform.platform(),)])
|
|
115
|
+
yield from _section(paint, "location", [(str(pathlib.Path(risa.__file__).parent),)])
|
|
116
|
+
yield ""
|
|
117
|
+
yield from _section(paint, "requires", required)
|
|
118
|
+
yield ""
|
|
119
|
+
yield from _section(paint, "extras", extras)
|
|
120
|
+
yield ""
|
|
121
|
+
yield from _section(paint, "docs", [(urls["Documentation"],)])
|
|
122
|
+
yield from _section(paint, "issues", [(urls["Issues"],)])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
"""Print the versions of risa, of Python and of the packages risa installs."""
|
|
127
|
+
parser = argparse.ArgumentParser(
|
|
128
|
+
prog="hikari-risa", description="Print the versions of risa, Python and what risa installs, for a bug report."
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument("--version", action="version", version=f"hikari-risa {risa.__version__}")
|
|
131
|
+
parser.parse_args()
|
|
132
|
+
colour = (
|
|
133
|
+
sys.stdout.isatty()
|
|
134
|
+
and not os.environ.get("NO_COLOR")
|
|
135
|
+
and os.environ.get("TERM") != "dumb"
|
|
136
|
+
and _escape_codes_work()
|
|
137
|
+
)
|
|
138
|
+
report = "\n".join(_report(colour=colour)) + "\n"
|
|
139
|
+
# Before 3.15 a Windows pipe writes in the ANSI code page, which may lack a character of the path.
|
|
140
|
+
encoding = sys.stdout.encoding or "utf-8"
|
|
141
|
+
sys.stdout.write(report.encode(encoding, "backslashreplace").decode(encoding))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
main()
|
risa/_about.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright (c) 2025 Rayakame
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
# copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""Package metadata, re-exported by `risa`."""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import typing
|
|
25
|
+
|
|
26
|
+
__author__: typing.Final[str] = "Rayakame"
|
|
27
|
+
__copyright__: typing.Final[str] = "2025, Rayakame"
|
|
28
|
+
__discord_invite__: typing.Final[str] = "https://discord.gg/Jx4cNGG"
|
|
29
|
+
__url__: typing.Final[str] = "https://github.com/rayakame/hikari-risa"
|
|
30
|
+
__version__: typing.Final[str] = "1.0.0"
|
|
31
|
+
__license__: typing.Final[str] = "MIT"
|
risa/binding.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Copyright (c) 2025 Rayakame
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
# copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""Putting handlers on components, with or without wire arguments."""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import functools
|
|
25
|
+
import typing
|
|
26
|
+
|
|
27
|
+
import msgspec
|
|
28
|
+
|
|
29
|
+
from risa import errors
|
|
30
|
+
|
|
31
|
+
if typing.TYPE_CHECKING:
|
|
32
|
+
import collections.abc
|
|
33
|
+
|
|
34
|
+
from risa import view as view_
|
|
35
|
+
from risa.internal import codec
|
|
36
|
+
|
|
37
|
+
__all__ = (
|
|
38
|
+
"BindTarget",
|
|
39
|
+
"Binding",
|
|
40
|
+
"Frame",
|
|
41
|
+
"HandlerBinder",
|
|
42
|
+
"Handlerish",
|
|
43
|
+
"bind",
|
|
44
|
+
"resolve",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
type BindTarget[**P] = collections.abc.Callable[P, collections.abc.Awaitable[None]]
|
|
48
|
+
"""A handler accessed on a view, as a type checker sees it: a callable of its wire parameters."""
|
|
49
|
+
|
|
50
|
+
type Handlerish = (
|
|
51
|
+
collections.abc.Callable[..., collections.abc.Awaitable[None]]
|
|
52
|
+
| Binding
|
|
53
|
+
| functools.partial[collections.abc.Awaitable[None]]
|
|
54
|
+
)
|
|
55
|
+
"""What a component takes as its handler: `self.vote`, or `risa.bind(self.vote, 2)`."""
|
|
56
|
+
|
|
57
|
+
bind: typing.Final[type[functools.partial[collections.abc.Awaitable[None]]]] = functools.partial
|
|
58
|
+
"""Put a handler on a component together with its wire arguments.
|
|
59
|
+
|
|
60
|
+
`ui.Button(risa.bind(self.vote, 2))` routes a click on that button to
|
|
61
|
+
`vote(ctx, 2)`. It is `functools.partial`, so arguments go by position or by name,
|
|
62
|
+
and nested `bind` calls merge as partials do: a keyword given twice takes the outer
|
|
63
|
+
value.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Frame(msgspec.Struct, frozen=True):
|
|
68
|
+
"""One wire argument of a `Binding`, encoded.
|
|
69
|
+
|
|
70
|
+
Attributes
|
|
71
|
+
----------
|
|
72
|
+
name
|
|
73
|
+
The parameter it fills.
|
|
74
|
+
data
|
|
75
|
+
The encoded value.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
name: str
|
|
79
|
+
data: bytes
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Binding(msgspec.Struct, frozen=True):
|
|
83
|
+
"""A handler resolved for one component: where a click routes, and what it carries.
|
|
84
|
+
|
|
85
|
+
Components resolve the handler they are given into one; it is rarely built by hand.
|
|
86
|
+
|
|
87
|
+
Attributes
|
|
88
|
+
----------
|
|
89
|
+
handler_id
|
|
90
|
+
The id of the handler.
|
|
91
|
+
version
|
|
92
|
+
The version of the handler.
|
|
93
|
+
token
|
|
94
|
+
The routing token of the handler.
|
|
95
|
+
fingerprint
|
|
96
|
+
The hash of the handler's wire signature, empty without wire arguments.
|
|
97
|
+
frames
|
|
98
|
+
The encoded wire arguments.
|
|
99
|
+
func_name
|
|
100
|
+
The handler's qualified name, for messages.
|
|
101
|
+
owner
|
|
102
|
+
The view class the handler is defined on.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
handler_id: str
|
|
106
|
+
version: int
|
|
107
|
+
token: str
|
|
108
|
+
fingerprint: str
|
|
109
|
+
frames: collections.abc.Sequence[Frame]
|
|
110
|
+
func_name: str
|
|
111
|
+
owner: type[view_.View] | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class HandlerBinder:
|
|
115
|
+
"""A handler accessed on a view instance, as `self.vote`.
|
|
116
|
+
|
|
117
|
+
Put it on a component bare, or with wire arguments through `risa.bind`. Calling it
|
|
118
|
+
raises `HandlerNotCallableError`, since only risa dispatches handlers.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
__slots__ = ("_func_name", "_handler_id", "_owner", "_signature", "_token", "_version")
|
|
122
|
+
|
|
123
|
+
def __init__(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
handler_id: str,
|
|
127
|
+
version: int,
|
|
128
|
+
token: str,
|
|
129
|
+
signature: codec.HandlerSignature,
|
|
130
|
+
func_name: str,
|
|
131
|
+
owner: type[view_.View] | None,
|
|
132
|
+
) -> None:
|
|
133
|
+
self._handler_id = handler_id
|
|
134
|
+
self._version = version
|
|
135
|
+
self._token = token
|
|
136
|
+
self._signature = signature
|
|
137
|
+
self._func_name = func_name
|
|
138
|
+
self._owner = owner
|
|
139
|
+
|
|
140
|
+
def __call__(self, *_args: object, **_kwargs: object) -> typing.NoReturn:
|
|
141
|
+
"""Raise `HandlerNotCallableError`, since only risa dispatches handlers."""
|
|
142
|
+
raise errors.HandlerNotCallableError(self._func_name)
|
|
143
|
+
|
|
144
|
+
def bind(self, *args: object, **kwargs: object) -> Binding:
|
|
145
|
+
"""Resolve this handler and its wire arguments into a `Binding`.
|
|
146
|
+
|
|
147
|
+
`risa.bind(self.vote, 2)` ends up here when the component is built.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
*args
|
|
152
|
+
The wire arguments, in the order of the handler's wire parameters.
|
|
153
|
+
**kwargs
|
|
154
|
+
Wire arguments by parameter name. An argument may be left out only if its
|
|
155
|
+
parameter has a default and every later one is left out too; this holds for
|
|
156
|
+
positional arguments as well.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
:
|
|
161
|
+
The handler's identity and its encoded arguments.
|
|
162
|
+
|
|
163
|
+
Raises
|
|
164
|
+
------
|
|
165
|
+
ArgBindError
|
|
166
|
+
If an argument names no wire parameter, is given twice, is missing, or holds a
|
|
167
|
+
value its parameter cannot carry.
|
|
168
|
+
"""
|
|
169
|
+
names = list(self._signature.converters)
|
|
170
|
+
if len(args) > len(names):
|
|
171
|
+
raise errors.ArgBindError(
|
|
172
|
+
self._func_name,
|
|
173
|
+
None,
|
|
174
|
+
f"received {len(args)} wire {'argument' if len(args) == 1 else 'arguments'}, "
|
|
175
|
+
f"but the handler declares {len(names)}",
|
|
176
|
+
)
|
|
177
|
+
filled: dict[str, object] = dict(zip(names, args, strict=False))
|
|
178
|
+
for name, value in kwargs.items():
|
|
179
|
+
if name not in self._signature.converters:
|
|
180
|
+
raise errors.ArgBindError(self._func_name, name, "is not a wire parameter of this handler")
|
|
181
|
+
if name in filled:
|
|
182
|
+
raise errors.ArgBindError(self._func_name, name, "was supplied both positionally and by keyword")
|
|
183
|
+
filled[name] = value
|
|
184
|
+
|
|
185
|
+
k = len(names)
|
|
186
|
+
for index, name in enumerate(names):
|
|
187
|
+
if name not in filled:
|
|
188
|
+
k = index
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if any(name in filled for name in names[k:]):
|
|
192
|
+
raise errors.ArgBindError(self._func_name, names[k], "was not supplied, but a later wire parameter was")
|
|
193
|
+
if k < self._signature.required:
|
|
194
|
+
raise errors.ArgBindError(self._func_name, names[k], "is required but was not supplied")
|
|
195
|
+
|
|
196
|
+
return Binding(
|
|
197
|
+
handler_id=self._handler_id,
|
|
198
|
+
version=self._version,
|
|
199
|
+
token=self._token,
|
|
200
|
+
fingerprint=self._signature.fingerprint if names else "",
|
|
201
|
+
frames=self._encode_frames(names[:k], filled),
|
|
202
|
+
func_name=self._func_name,
|
|
203
|
+
owner=self._owner,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _encode_frames(
|
|
207
|
+
self,
|
|
208
|
+
names: collections.abc.Sequence[str],
|
|
209
|
+
filled: dict[str, object],
|
|
210
|
+
) -> collections.abc.Sequence[Frame]:
|
|
211
|
+
frames: list[Frame] = []
|
|
212
|
+
for name in names:
|
|
213
|
+
try:
|
|
214
|
+
data = self._signature.converters[name].encode(filled[name])
|
|
215
|
+
except (TypeError, ValueError) as exc:
|
|
216
|
+
raise errors.ArgBindError(self._func_name, name, str(exc)) from exc
|
|
217
|
+
frames.append(Frame(name=name, data=data))
|
|
218
|
+
return tuple(frames)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def resolve(handler: Handlerish) -> Binding:
|
|
222
|
+
"""Return the `Binding` that `handler` routes to.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
handler
|
|
227
|
+
A handler accessed on a view instance, as `self.vote`, bare or through `risa.bind`.
|
|
228
|
+
A `Binding` is accepted and returned as it is.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
:
|
|
233
|
+
The handler's identity and its encoded arguments.
|
|
234
|
+
|
|
235
|
+
Raises
|
|
236
|
+
------
|
|
237
|
+
NotAHandlerError
|
|
238
|
+
If `handler` is not a handler accessed on a view instance, bare or bound.
|
|
239
|
+
ArgBindError
|
|
240
|
+
If its wire arguments do not fit the handler.
|
|
241
|
+
"""
|
|
242
|
+
if isinstance(handler, Binding):
|
|
243
|
+
return handler
|
|
244
|
+
if isinstance(handler, functools.partial):
|
|
245
|
+
inner = handler.func
|
|
246
|
+
if not isinstance(inner, HandlerBinder):
|
|
247
|
+
raise errors.NotAHandlerError(type(inner).__name__)
|
|
248
|
+
return inner.bind(*handler.args, **handler.keywords)
|
|
249
|
+
if isinstance(handler, HandlerBinder):
|
|
250
|
+
return handler.bind()
|
|
251
|
+
raise errors.NotAHandlerError(type(handler).__name__)
|