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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hikari-risa = risa.__main__:main
@@ -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__)