matrix-python 1.4.8a0__py3-none-any.whl → 1.4.10a0__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.
matrix/_version.py CHANGED
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.4.8a0'
22
- __version_tuple__ = version_tuple = (1, 4, 8, 'a0')
21
+ __version__ = version = '1.4.10a0'
22
+ __version_tuple__ = version_tuple = (1, 4, 10, 'a0')
23
23
 
24
24
  __commit_id__ = commit_id = None
matrix/checks.py CHANGED
@@ -8,10 +8,18 @@ def cooldown(rate: int, period: float) -> Callable:
8
8
  """
9
9
  Decorator to cooldown a command.
10
10
 
11
- :param rate: The number of request a user can send.
12
- :type rate: int
13
- :param period: The period in seconds of the cooldown.
14
- :type period: float
11
+ ## Example
12
+
13
+ ```python
14
+ @cooldown(rate=3, period=10)
15
+ @bot.command("hello")
16
+ async def hello(ctx: Context) -> None:
17
+ await ctx.reply("Hello!")
18
+
19
+ @hello.error(CooldownError)
20
+ async def hello_error(ctx: Context, error: CooldownError) -> None:
21
+ await ctx.reply(f"Slow down! Try again in {error.retry:.1f}s.")
22
+ ```
15
23
  """
16
24
 
17
25
  def wrapper(cmd: "Command") -> "Command":
matrix/command.py CHANGED
@@ -30,19 +30,6 @@ ErrorCallback = Callable[["Context", Exception], Coroutine[Any, Any, Any]]
30
30
  class Command:
31
31
  """
32
32
  Represents a command that can be executed with a context and arguments.
33
-
34
- :param func: The coroutine that is executed when the command is invoked.
35
- :type func: Callable[..., Coroutine[Any, Any, Any]]
36
-
37
- :param name: Optional name. Defaults to the function's name.
38
- :param description: Optional description of what the command does.
39
- :param prefix: Optional prefix for the command.
40
- :param parent: Optional parent command name for subcommands.
41
- :param usage: Optional usage string for the command.
42
- :param cooldown: Optional cooldown settings as a tuple of (rate, period).
43
-
44
- :raises TypeError: If the provided name is not a string.
45
- :raises TypeError: If the provided callback is not a coroutine.
46
33
  """
47
34
 
48
35
  def __init__(
@@ -85,9 +72,6 @@ class Command:
85
72
  def callback(self) -> Callback:
86
73
  """
87
74
  Returns the coroutine function for this command.
88
-
89
- :return: The command's coroutine function.
90
- :rtype: Callback
91
75
  """
92
76
  return self._callback
93
77
 
@@ -96,10 +80,6 @@ class Command:
96
80
  """
97
81
  Sets the coroutine function for the command and extracts type
98
82
  hints and parameters.
99
-
100
- :param func: The coroutine function to use.
101
- :type func: Callback
102
- :raises TypeError: If the provided function is not a coroutine.
103
83
  """
104
84
  if not inspect.iscoroutinefunction(func):
105
85
  raise TypeError("Commands must be coroutines")
@@ -113,9 +93,6 @@ class Command:
113
93
  def _build_help(self) -> str:
114
94
  """
115
95
  Returns the help text for the command.
116
-
117
- :return: The help text for the command.
118
- :rtype: str
119
96
  """
120
97
  default_help = f"{self.description}\n\nusage: {self.usage}"
121
98
  return inspect.cleandoc(default_help)
@@ -124,9 +101,6 @@ class Command:
124
101
  """
125
102
  Builds and returns the default usage string for the command.
126
103
  set at the command initalization.
127
-
128
- :return: A usage string.
129
- :rtype: str
130
104
  """
131
105
  params = " ".join(f"[{p.name}]" for p in self.params)
132
106
  command_name = self.name
@@ -186,10 +160,17 @@ class Command:
186
160
  """
187
161
  Register a check callback
188
162
 
189
- :param func: The check callback
190
- :type func: Callback
163
+ ## Example
164
+
165
+ ```python
166
+ @bot.command("secret")
167
+ async def secret(ctx: Context) -> None:
168
+ await ctx.reply("Access granted!")
191
169
 
192
- :raises TypeError: If the function is not a coroutine.
170
+ @secret.check
171
+ async def is_allowed(ctx: Context) -> bool:
172
+ return ctx.sender == "@admin:matrix.org"
173
+ ```
193
174
  """
194
175
  if not inspect.iscoroutinefunction(func):
195
176
  raise TypeError("Checks must be coroutine")
@@ -227,10 +208,17 @@ class Command:
227
208
  """
228
209
  Registers a coroutine to be called before the command is invoked.
229
210
 
230
- :param func: The coroutine function to call before command invocation.
231
- :type func: Callback
211
+ ## Example
232
212
 
233
- :raises TypeError: If the function is not a coroutine.
213
+ ```python
214
+ @bot.command("ping")
215
+ async def ping(ctx: Context) -> None:
216
+ await ctx.reply("Pong!")
217
+
218
+ @ping.before_invoke
219
+ async def before_ping(ctx: Context) -> None:
220
+ print(f"ping invoked by {ctx.sender}")
221
+ ```
234
222
  """
235
223
 
236
224
  if not inspect.iscoroutinefunction(func):
@@ -242,10 +230,17 @@ class Command:
242
230
  """
243
231
  Registers a coroutine to be called after the command is invoked.
244
232
 
245
- :param func: The coroutine function to call after command execution.
246
- :type func: Callback
233
+ ## Example
234
+
235
+ ```python
236
+ @bot.command("ping")
237
+ async def ping(ctx: Context) -> None:
238
+ await ctx.reply("Pong!")
247
239
 
248
- :raises TypeError: If the function is not a coroutine.
240
+ @ping.after_invoke
241
+ async def after_ping(ctx: Context) -> None:
242
+ print(f"ping completed for {ctx.sender}")
243
+ ```
249
244
  """
250
245
 
251
246
  if not inspect.iscoroutinefunction(func):
@@ -257,11 +252,21 @@ class Command:
257
252
  """
258
253
  Decorator used to register an error handler for this command.
259
254
 
260
- :param exception: Exception type to register the handler for.
261
- :type exception: Optional[Exception]
262
- :return: A decorator that registers the provided coroutine as an
263
- error handler and returns the original function.
264
- :rtype: Callable
255
+ ## Example
256
+
257
+ ```python
258
+ @bot.command("div")
259
+ async def div(ctx: Context, a: int, b: int) -> None:
260
+ await ctx.reply(f"{a / b}")
261
+
262
+ @div.error(ZeroDivisionError)
263
+ async def div_error(ctx: Context, error: ZeroDivisionError) -> None:
264
+ await ctx.reply("Cannot divide by zero!")
265
+
266
+ @div.error(MissingArgumentError)
267
+ async def div_missing(ctx: Context, error: MissingArgumentError) -> None:
268
+ await ctx.reply(f"Missing argument: {error}")
269
+ ```
265
270
  """
266
271
 
267
272
  def wrapper(func: ErrorCallback) -> Callable:
@@ -279,11 +284,6 @@ class Command:
279
284
  async def on_error(self, ctx: "Context", error: Exception) -> None:
280
285
  """
281
286
  Executes the registered error handler if present.
282
-
283
- :param ctx: The command execution context.
284
- :type ctx: Context
285
- :param error: The exception that was raised.
286
- :type error: Exception
287
287
  """
288
288
 
289
289
  if handler := self._error_handlers.get(type(error)):
@@ -322,9 +322,6 @@ class Command:
322
322
  async def __call__(self, ctx: "Context") -> None:
323
323
  """
324
324
  Execute the command with parsed arguments.
325
-
326
- :param ctx: The command execution context.
327
- :type ctx: Context
328
325
  """
329
326
  await self._invoke(ctx)
330
327
 
matrix/context.py CHANGED
@@ -38,9 +38,6 @@ class Context:
38
38
  Returns the list of parsed arguments from the message body.
39
39
 
40
40
  If a command is present, the command name is excluded.
41
-
42
- :return: The list of arguments.
43
- :rtype: List[str]
44
41
  """
45
42
  if self.subcommand:
46
43
  return self._args[2:]
matrix/extension.py CHANGED
@@ -1,21 +1,31 @@
1
1
  import inspect
2
2
  import logging
3
- from typing import Callable, Optional
3
+ from typing import Callable
4
4
 
5
5
  from matrix.protocols import BotLike
6
6
  from matrix.registry import Registry
7
+ from matrix.config import Config
7
8
  from matrix.room import Room
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
11
12
 
12
13
  class Extension(Registry):
13
- def __init__(self, name: str, prefix: Optional[str] = None) -> None:
14
+ def __init__(self, name: str, prefix: str | None = None) -> None:
14
15
  super().__init__(name, prefix=prefix)
15
16
 
16
- self.bot: Optional[BotLike] = None
17
- self._on_load: Optional[Callable] = None
18
- self._on_unload: Optional[Callable] = None
17
+ self._bot: BotLike | None = None
18
+ self._on_load: Callable | None = None
19
+ self._on_unload: Callable | None = None
20
+
21
+ @property
22
+ def bot(self) -> BotLike:
23
+ assert self._bot, "Extension is not loaded"
24
+ return self._bot
25
+
26
+ @property
27
+ def config(self) -> Config:
28
+ return self.bot.config
19
29
 
20
30
  def get_room(self, room_id: str) -> Room | None:
21
31
  """Retrieve a `Room` instance by its Matrix room ID.
@@ -31,12 +41,10 @@ class Extension(Registry):
31
41
  print(room.name)
32
42
  ```
33
43
  """
34
- if self.bot is None:
35
- raise RuntimeError("Extension is not loaded")
36
44
  return self.bot.get_room(room_id)
37
45
 
38
46
  def load(self, bot: BotLike) -> None:
39
- self.bot = bot
47
+ self._bot = bot
40
48
 
41
49
  if self._on_load:
42
50
  self._on_load()
@@ -59,7 +67,7 @@ class Extension(Registry):
59
67
  return func
60
68
 
61
69
  def unload(self) -> None:
62
- self.bot = None
70
+ self._bot = None
63
71
 
64
72
  if self._on_unload:
65
73
  self._on_unload()
matrix/group.py CHANGED
@@ -52,13 +52,21 @@ class Group(Command):
52
52
  The command name defaults to the function name unless
53
53
  explicitly provided.
54
54
 
55
- :param name: The name of the command. If omitted, the function
56
- name is used.
57
- :type name: str, optional
58
- :raises TypeError: If the decorated function is not a coroutine.
59
- :raises ValueError: If a command with the same name is registered.
60
- :return: Decorator that registers the command handler.
61
- :rtype: Callback
55
+ ## Example
56
+
57
+ ```python
58
+ @bot.group("math")
59
+ async def math(ctx: Context) -> None:
60
+ await ctx.send_help()
61
+
62
+ @math.command()
63
+ async def add(ctx: Context, a: int, b: int) -> None:
64
+ await ctx.reply(f"{a + b}")
65
+
66
+ @math.command("sub")
67
+ async def subtract(ctx: Context, a: int, b: int) -> None:
68
+ await ctx.reply(f"{a - b}")
69
+ ```
62
70
  """
63
71
 
64
72
  def wrapper(func: Callback) -> Command:
@@ -17,11 +17,7 @@ class HelpCommand(Command, ABC):
17
17
  DEFAULT_PER_PAGE = 5
18
18
 
19
19
  def __init__(self, prefix: Optional[str] = None, per_page: int = DEFAULT_PER_PAGE):
20
- """Initialize the help command.
21
-
22
- :param prefix: Command prefix override
23
- :param per_page: Number of commands to display per page
24
- """
20
+ """Initialize the help command."""
25
21
  super().__init__(
26
22
  self.execute,
27
23
  name="help",
@@ -32,18 +28,48 @@ class HelpCommand(Command, ABC):
32
28
 
33
29
  @abstractmethod
34
30
  def format_help_page(self, page: Page[Command], title: str = "Commands") -> str:
31
+ """Format a full page of commands into a displayable string.
32
+
33
+ ## Example
34
+
35
+ ```python
36
+ def format_help_page(self, page, title="Commands") -> str:
37
+ lines = [f"**{title}**"]
38
+ for cmd in page.items:
39
+ lines.append(self.format_command(cmd))
40
+ lines.append(self.format_page_info(page))
41
+ return "\\n".join(lines)
42
+ ```
43
+ """
35
44
  pass # pragma: no cover
36
45
 
37
46
  @abstractmethod
38
47
  def format_subcommand_page(self, page: Page[Command], group_name: str) -> str:
48
+ """Format a full page of subcommands for a group into a displayable string.
49
+
50
+ ## Example
51
+
52
+ ```python
53
+ def format_subcommand_page(self, page, group_name) -> str:
54
+ lines = [f"**{group_name} subcommands**"]
55
+ for cmd in page.items:
56
+ lines.append(self.format_subcommand(cmd))
57
+ lines.append(self.format_page_info(page))
58
+ return "\\n".join(lines)
59
+ ```
60
+ """
39
61
  pass # pragma: no cover
40
62
 
41
63
  @abstractmethod
42
64
  def format_command(self, cmd: Command) -> str:
43
65
  """Format a single command for display.
44
66
 
45
- :param cmd: The command to format
46
- :return: Formatted string representation of the command
67
+ ## Example
68
+
69
+ ```python
70
+ def format_command(self, cmd) -> str:
71
+ return f"**{cmd.name}** — {cmd.description}"
72
+ ```
47
73
  """
48
74
  pass # pragma: no cover
49
75
 
@@ -51,8 +77,12 @@ class HelpCommand(Command, ABC):
51
77
  def format_group(self, group: Group) -> str: # pragma: no cover
52
78
  """Format a group command for display.
53
79
 
54
- :param group: The group to format
55
- :return: Formatted string representation of the group
80
+ ## Example
81
+
82
+ ```python
83
+ def format_group(self, group) -> str:
84
+ return f"**{group.name}** [group] — {group.description}"
85
+ ```
56
86
  """
57
87
  pass
58
88
 
@@ -60,8 +90,12 @@ class HelpCommand(Command, ABC):
60
90
  def format_subcommand(self, subcommand: Command) -> str:
61
91
  """Format a subcommand for display.
62
92
 
63
- :param subcommand: The subcommand to format
64
- :return: Formatted string representation of the subcommand
93
+ ## Example
94
+
95
+ ```python
96
+ def format_subcommand(self, subcommand) -> str:
97
+ return f" **{subcommand.name}** — {subcommand.description}"
98
+ ```
65
99
  """
66
100
  pass # pragma: no cover
67
101
 
@@ -69,8 +103,12 @@ class HelpCommand(Command, ABC):
69
103
  def format_page_info(self, page: Page[Command]) -> str:
70
104
  """Format the page information display.
71
105
 
72
- :param page: Page object containing pagination info
73
- :return: Formatted page information string
106
+ ## Example
107
+
108
+ ```python
109
+ def format_page_info(self, page) -> str:
110
+ return f"Page {page.page_number}/{page.total_pages}"
111
+ ```
74
112
  """
75
113
  pass # pragma: no cover
76
114
 
@@ -98,43 +136,25 @@ class HelpCommand(Command, ABC):
98
136
  pass # pragma: no cover
99
137
 
100
138
  def get_commands_paginator(self, ctx: Context) -> Paginator[Command]:
101
- """Get a paginator for all commands.
102
-
103
- :param ctx: Command context
104
- :return: Paginator configured with all commands
105
- """
139
+ """Get a paginator for all commands."""
106
140
  all_commands = list(ctx.bot.commands.values())
107
141
  sorted_commands = sorted(all_commands, key=lambda c: c.name.lower())
108
142
 
109
143
  return Paginator(sorted_commands, self.per_page)
110
144
 
111
145
  def get_subcommands_paginator(self, group: Group) -> Paginator[Command]:
112
- """Get a paginator for all subcommands in a group.
113
-
114
- :param group: The group to get subcommands from
115
- :return: Paginator configured with all subcommands
116
- """
146
+ """Get a paginator for all subcommands in a group."""
117
147
  subcommands = list(getattr(group, "commands", {}).values())
118
148
  sorted_subcommands = sorted(subcommands, key=lambda c: c.name.lower())
119
149
 
120
150
  return Paginator(sorted_subcommands, self.per_page)
121
151
 
122
152
  def find_command(self, ctx: Context, command_name: str) -> Optional[Command]:
123
- """Find a command by name.
124
-
125
- :param ctx: Command context
126
- :param command_name: Name of the command to find
127
- :return: Command if found, None otherwise
128
- """
153
+ """Find a command by name."""
129
154
  return ctx.bot.commands.get(command_name)
130
155
 
131
156
  def find_subcommand(self, group: Group, subcommand_name: str) -> Optional[Command]:
132
- """Find a subcommand within a group.
133
-
134
- :param group: The group to search in
135
- :param subcommand_name: Name of the subcommand to find
136
- :return: Subcommand if found, None otherwise
137
- """
157
+ """Find a subcommand within a group."""
138
158
  group_commands = getattr(group, "commands", {})
139
159
  return group_commands.get(subcommand_name)
140
160
 
@@ -175,11 +195,7 @@ class HelpCommand(Command, ABC):
175
195
  await self.on_empty_page(ctx)
176
196
 
177
197
  async def show_help_page(self, ctx: Context, page_number: int = 1) -> None:
178
- """Show a paginated help page for all commands.
179
-
180
- :param ctx: Command context
181
- :param page_number: Page number to display
182
- """
198
+ """Show a paginated help page for all commands."""
183
199
  paginator = self.get_commands_paginator(ctx)
184
200
  page = paginator.get_page(page_number)
185
201
  help_message = self.format_help_page(page)
@@ -271,12 +287,7 @@ class DefaultHelpCommand(HelpCommand):
271
287
  """
272
288
 
273
289
  def format_help_page(self, page: Page[Command], title: str = "Commands") -> str:
274
- """Format a complete help page.
275
-
276
- :param page: Page object containing commands and pagination info
277
- :param title: Title for the help page
278
- :return: Complete formatted help page
279
- """
290
+ """Format a complete help page."""
280
291
  help_entries = []
281
292
 
282
293
  if not page.items:
@@ -294,12 +305,7 @@ class DefaultHelpCommand(HelpCommand):
294
305
  return f"**{title}**\n\n{help_text}\n\n{page_info}"
295
306
 
296
307
  def format_subcommand_page(self, page: Page[Command], group_name: str) -> str:
297
- """Format a complete subcommand help page.
298
-
299
- :param page: Page object containing subcommands and pagination info
300
- :param group_name: Name of the parent group
301
- :return: Complete formatted subcommand help page
302
- """
308
+ """Format a complete subcommand help page."""
303
309
  help_entries = []
304
310
 
305
311
  if not page.items:
@@ -314,11 +320,7 @@ class DefaultHelpCommand(HelpCommand):
314
320
  return f"**{group_name} Subcommands**\n\n{help_text}\n\n{page_info}"
315
321
 
316
322
  def format_command(self, cmd: Command) -> str:
317
- """Format a single command for display.
318
-
319
- :param cmd: The command to format
320
- :return: Formatted string representation of the command
321
- """
323
+ """Format a single command for display."""
322
324
  return (
323
325
  f"**{cmd.name}**\n"
324
326
  f"Usage: `{cmd.usage}`\n"
@@ -326,11 +328,7 @@ class DefaultHelpCommand(HelpCommand):
326
328
  )
327
329
 
328
330
  def format_group(self, group: Group) -> str:
329
- """Format a group command for display.
330
-
331
- :param group: The group to format
332
- :return: Formatted string representation of the group
333
- """
331
+ """Format a group command for display."""
334
332
  subcommands_text = ""
335
333
  subcommand_count = len(getattr(group, "commands", {}))
336
334
 
@@ -344,11 +342,7 @@ class DefaultHelpCommand(HelpCommand):
344
342
  )
345
343
 
346
344
  def format_subcommand(self, subcommand: Command) -> str:
347
- """Format a subcommand for display.
348
-
349
- :param subcommand: The subcommand to format
350
- :return: Formatted string representation of the subcommand
351
- """
345
+ """Format a subcommand for display."""
352
346
  return (
353
347
  f"**{subcommand.name}**\n"
354
348
  f"Usage: `{subcommand.usage}`\n"
@@ -356,11 +350,7 @@ class DefaultHelpCommand(HelpCommand):
356
350
  )
357
351
 
358
352
  def format_page_info(self, page: Page[Command]) -> str:
359
- """Format the page information display.
360
-
361
- :param page: Page object containing pagination info
362
- :return: Formatted page information string
363
- """
353
+ """Format the page information display."""
364
354
  return f"**Page {page.page_number}/{page.total_pages}**"
365
355
 
366
356
  async def on_command_not_found(self, ctx: Context, command_name: str) -> None:
matrix/help/pagination.py CHANGED
@@ -7,22 +7,14 @@ class Paginator(Generic[T]):
7
7
  """A generic paginator for any list of items."""
8
8
 
9
9
  def __init__(self, items: List[T], per_page: int = 5):
10
- """Initialize the paginator.
11
-
12
- :param items: List of items to paginate
13
- :param per_page: Number of items per page
14
- """
10
+ """Initialize the paginator."""
15
11
  self.items = items
16
12
  self.per_page = per_page
17
13
  self.total_items = len(items)
18
14
  self.total_pages = max(1, -(-self.total_items // self.per_page))
19
15
 
20
16
  def get_page(self, page_number: int) -> "Page[T]":
21
- """Get a specific page of items.
22
-
23
- :param page_number: Page number to retrieve (1-indexed)
24
- :return: Page object containing items and metadata
25
- """
17
+ """Get a specific page of items."""
26
18
  # Clamp page number to valid range
27
19
  page_number = max(1, min(page_number, self.total_pages))
28
20
 
@@ -38,10 +30,7 @@ class Paginator(Generic[T]):
38
30
  )
39
31
 
40
32
  def get_pages(self) -> List["Page[T]"]:
41
- """Get all pages.
42
-
43
- :return: List of all pages
44
- """
33
+ """Get all pages."""
45
34
  return [self.get_page(i) for i in range(1, self.total_pages + 1)]
46
35
 
47
36
 
@@ -56,14 +45,7 @@ class Page(Generic[T]):
56
45
  per_page: int,
57
46
  total_items: int,
58
47
  ):
59
- """Initialize a page.
60
-
61
- :param items: Items on this page
62
- :param page_number: Current page number
63
- :param total_pages: Total number of pages
64
- :param per_page: Items per page
65
- :param total_items: Total number of items across all pages
66
- """
48
+ """Initialize a page."""
67
49
  self.items = items
68
50
  self.page_number = page_number
69
51
  self.total_pages = total_pages
matrix/protocols.py CHANGED
@@ -1,9 +1,13 @@
1
1
  from typing import Protocol
2
2
 
3
+ from matrix.config import Config
3
4
  from matrix.room import Room
4
5
 
5
6
 
6
7
  class BotLike(Protocol):
7
8
  prefix: str | None
8
9
 
10
+ @property
11
+ def config(self) -> Config: ...
12
+
9
13
  def get_room(self, room_id: str) -> Room | None: ...
matrix/registry.py CHANGED
@@ -2,7 +2,20 @@ import inspect
2
2
  import logging
3
3
 
4
4
  from collections import defaultdict
5
- from typing import Any, Callable, Coroutine, Optional, Type, Union, Dict, List
5
+ from typing import (
6
+ TypeVar,
7
+ Any,
8
+ Callable,
9
+ Coroutine,
10
+ Literal,
11
+ Optional,
12
+ Type,
13
+ Union,
14
+ Dict,
15
+ List,
16
+ cast,
17
+ overload,
18
+ )
6
19
 
7
20
  from nio import (
8
21
  Event,
@@ -25,6 +38,8 @@ GroupCallable = Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group]
25
38
  ErrorCallback = Callable[[Exception], Coroutine]
26
39
  CommandErrorCallback = Callable[[Context, Exception], Coroutine[Any, Any, Any]]
27
40
 
41
+ F = TypeVar("F", ErrorCallback, CommandErrorCallback)
42
+
28
43
 
29
44
  class Registry:
30
45
  """
@@ -355,43 +370,91 @@ class Registry:
355
370
 
356
371
  return wrapper
357
372
 
373
+ @overload
358
374
  def error(
359
- self, exception: Optional[type[Exception]] = None
360
- ) -> Callable[[ErrorCallback], ErrorCallback]:
375
+ self,
376
+ exception: Optional[type[Exception]] = None,
377
+ *,
378
+ context: Literal[True],
379
+ ) -> Callable[[CommandErrorCallback], CommandErrorCallback]: ...
380
+
381
+ @overload
382
+ def error(
383
+ self,
384
+ exception: Optional[type[Exception]] = None,
385
+ *,
386
+ context: Literal[False] = ...,
387
+ ) -> Callable[[ErrorCallback], ErrorCallback]: ...
388
+
389
+ def error(
390
+ self,
391
+ exception: Optional[type[Exception]] = None,
392
+ *,
393
+ context: bool = False,
394
+ ) -> Union[
395
+ Callable[[ErrorCallback], ErrorCallback],
396
+ Callable[[CommandErrorCallback], CommandErrorCallback],
397
+ ]:
361
398
  """Decorator to register an error handler.
362
399
 
363
400
  If an exception type is provided, the handler is only invoked for
364
401
  that specific exception. If omitted, the handler acts as a generic
365
402
  fallback for any unhandled error.
366
403
 
404
+ Set ``context=True`` to receive the command context alongside the error,
405
+ useful for command-specific errors where you want to reply to the user.
406
+
367
407
  ## Example
368
408
 
369
409
  ```python
370
410
  @bot.error(ValueError)
371
411
  async def on_value_error(error):
372
- await room.send(f"Bad value: {error}")
412
+ pass
373
413
 
374
414
  @bot.error()
375
415
  async def on_any_error(error):
376
- await room.send(f"Something went wrong: {error}")
416
+ pass
417
+
418
+ @bot.error(CommandNotFoundError, context=True)
419
+ async def on_command_not_found(ctx, error):
420
+ await ctx.reply("Command not found!")
377
421
  ```
378
422
  """
379
423
 
380
- if not exception:
381
- exception = Exception
382
-
383
- def wrapper(func: ErrorCallback) -> ErrorCallback:
424
+ def wrapper(
425
+ func: F,
426
+ ) -> F:
384
427
  if not inspect.iscoroutinefunction(func):
385
428
  raise TypeError("Error handlers must be coroutines")
386
429
 
387
- self._error_handlers[exception] = func
430
+ if context:
431
+ self._register_command_error(
432
+ cast(CommandErrorCallback, func), exception
433
+ )
434
+ else:
435
+ self._register_error(cast(ErrorCallback, func), exception)
388
436
 
389
437
  logger.debug(
390
438
  "registered error handler '%s' on %s",
391
439
  func.__name__,
392
440
  type(self).__name__,
393
441
  )
394
-
395
442
  return func
396
443
 
397
444
  return wrapper
445
+
446
+ def _register_error(
447
+ self, func: ErrorCallback, exception: Optional[type[Exception]] = None
448
+ ) -> None:
449
+ if not exception:
450
+ exception = Exception
451
+ self._error_handlers[exception] = func
452
+
453
+ def _register_command_error(
454
+ self,
455
+ func: CommandErrorCallback,
456
+ exception: Optional[type[Exception]] = None,
457
+ ) -> None:
458
+ if not exception:
459
+ exception = Exception
460
+ self._command_error_handlers[exception] = func
matrix/scheduler.py CHANGED
@@ -21,11 +21,6 @@ class Scheduler:
21
21
  def _parse_cron(self, cron: str) -> dict:
22
22
  """
23
23
  Parse a cron string into a dictionary suitable for CronTrigger.
24
-
25
- :param cron: The cron string to parse.
26
- :type cron: str
27
- :return: A dictionary with cron fields.
28
- :rtype: dict
29
24
  """
30
25
  fields = cron.split()
31
26
  if len(fields) != 5:
@@ -42,10 +37,13 @@ class Scheduler:
42
37
  """
43
38
  Schedule a coroutine function to be run at specified intervals.
44
39
 
45
- :param cron: The cron string defining the schedule.
46
- :type cron: str
47
- :param func: The coroutine function to run.
48
- :type func: Callback
40
+ ## Example
41
+
42
+ ```python
43
+ @bot.schedule("0 9 * * 1-5")
44
+ async def morning_update() -> None:
45
+ await room.send("Good morning!")
46
+ ```
49
47
  """
50
48
  cron_trigger = CronTrigger(**self._parse_cron(cron))
51
49
  self.scheduler.add_job(func, trigger=cron_trigger, name=func.__name__)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.8a0
3
+ Version: 1.4.10a0
4
4
  Summary: An easy-to-use Matrix bot framework designed for quick development and minimal setup
5
5
  Author: Simon Roy, Chris Dedman Rollet
6
6
  Maintainer-email: Code Society Lab <admin@codesociety.xyz>
@@ -40,57 +40,72 @@ Provides-Extra: dev
40
40
  Requires-Dist: pytest==9.0.3; extra == "dev"
41
41
  Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
42
42
  Requires-Dist: black==26.3.1; extra == "dev"
43
- Requires-Dist: mypy==1.20.0; extra == "dev"
43
+ Requires-Dist: mypy==1.20.1; extra == "dev"
44
44
  Requires-Dist: types-PyYAML==6.0.12.20260408; extra == "dev"
45
45
  Requires-Dist: types-Markdown==3.10.2.20260408; extra == "dev"
46
+ Provides-Extra: doc
47
+ Requires-Dist: mkdocs==1.6.1; extra == "doc"
48
+ Requires-Dist: mkdocs-material==9.7.6; extra == "doc"
49
+ Requires-Dist: mkdocstrings[python]==1.0.4; extra == "doc"
46
50
 
47
51
  <div align="center">
48
52
  <em>A simple, developer-friendly library to create powerful <a href="https://matrix.org">Matrix</a> bots.</em>
49
53
  </div>
50
54
 
51
- <img alt="image" src="https://github.com/user-attachments/assets/d9140a9e-27fa-44e4-a5ca-87ee7bbf868f" />
55
+ <div align="center">
56
+ <img alt="matrix.py" src="https://github.com/user-attachments/assets/d9140a9e-27fa-44e4-a5ca-87ee7bbf868f" />
57
+ </div>
58
+
59
+ <div align="center">
52
60
 
53
- <hr />
61
+ [<img src="https://img.shields.io/badge/Get%20Started-black?style=for-the-badge" />](https://matrixpy.code-society.xyz/guides/introduction/)
62
+ [<img src="https://img.shields.io/badge/Reference-555555?style=for-the-badge" />](https://matrixpy.code-society.xyz/reference/bot/)
63
+
64
+ </div>
65
+
66
+ <div align="center">
54
67
 
55
- [![Static Badge](https://img.shields.io/badge/%F0%9F%93%9A-Documentation-%235c5c5c)](https://github.com/Code-Society-Lab/matrixpy/wiki)
56
68
  [![Join Discord](https://discordapp.com/api/guilds/823178343943897088/widget.png?style=shield)](https://discord.gg/code-society-823178343943897088)
57
- [![Join Matrix](https://img.shields.io/matrix/codesociety%3Amatrix.org?logo=matrix&label=%20&labelColor=%23202020&color=%23202020)](https://matrix.to/#/%23codesociety:matrix.org )
69
+ [![Join Matrix](https://img.shields.io/matrix/codesociety%3Amatrix.org?logo=matrix&label=%20&labelColor=%23202020&color=%23202020)](https://matrix.to/#/%23codesociety:matrix.org)
58
70
  [![Tests](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/tests.yml/badge.svg)](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/tests.yml)
59
71
  [![CodeQL Advanced](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/codeql.yml/badge.svg)](https://github.com/Code-Society-Lab/matrixpy/actions/workflows/codeql.yml)
60
72
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Code-Society-Lab/matrixpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Code-Society-Lab/matrixpy)
61
73
 
62
- Matrix.py is a lightweight and intuitive Python library to build bots on
63
- the [Matrix protocol](https://matrix.org). It provides a clean,
64
- decorator-based API similar to popular event-driven frameworks, allowing
65
- developers to focus on behavior rather than boilerplate.
74
+ </div>
66
75
 
67
- #### Key Features
76
+ ---
68
77
 
69
- - Minimal setup, easy to extend
70
- - Event-driven API using async/await
71
- - Clean command registration
72
- - Automatic event handler registration
73
- - Built on [matrix-nio](https://github.com/matrix-nio/matrix-nio)
78
+ Matrix.py is a lightweight and intuitive Python library to build bots on the [Matrix protocol](https://matrix.org). It
79
+ provides a clean, decorator-based API similar to popular event-driven frameworks, allowing developers to focus on
80
+ behavior rather than boilerplate.
74
81
 
75
- # Quickstart
82
+ - **Minimal setup** — install and have a working bot running in minutes
83
+ - **Event-driven** — async/await API reacting to any Matrix room event
84
+ - **Command system** — decorator-based commands with automatic argument parsing
85
+ - **Extensions** — split your bot into modules as it grows
76
86
 
77
- **Requirements**
87
+ ## Quickstart
78
88
 
79
- - Python 3.10+
89
+ **Requirements:** Python 3.10+
80
90
 
81
- ```
91
+ ```bash
82
92
  pip install matrix-python
83
93
  ```
84
94
 
85
- If you plan on contributing to matrix.py, we recommend to install the development libraries:
95
+ Using a virtual environment is strongly recommended:
86
96
 
87
- ```
88
- pip install -e .[dev]
97
+ ```bash
98
+ python -m venv venv
99
+ source venv/bin/activate # Windows: venv\Scripts\activate
100
+ pip install matrix-python
89
101
  ```
90
102
 
91
- *Note*: It is recommended to use
92
- a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/)
93
- when installing python packages.
103
+ Create a `config.yml`:
104
+
105
+ ```yaml
106
+ USERNAME: "@yourbot:matrix.org"
107
+ PASSWORD: "your_password"
108
+ ```
94
109
 
95
110
  ```python
96
111
  from matrix import Bot, Context
@@ -106,23 +121,29 @@ async def ping(ctx: Context):
106
121
  bot.start(config="config.yml")
107
122
  ```
108
123
 
109
- [Documentation](https://github.com/Code-Society-Lab/matrixpy/wiki) - [Examples](https://github.com/Code-Society-Lab/matrixpy/tree/main/examples)
124
+ Send `!ping` in any room the bot is in, it will reply `Pong!`.
125
+
126
+ ## Where to go next
110
127
 
111
- # Contributing
128
+ - [**Guides**](https://matrixpy.codesociety.xyz/guides/introduction/) — step-by-step tutorials covering commands,
129
+ events, checks, extensions, and more
130
+ - [**Reference**](https://matrixpy.codesociety.xyz/reference/bot/) — complete API documentation for every class and
131
+ function
132
+ - [**Examples**](https://matrixpy.codesociety.xyz/examples/) — ready-to-run example bots
133
+ demonstrating common patterns
112
134
 
113
- We welcome everyone to contribute!
135
+ ## Contributing
114
136
 
115
- Whether it's fixing bugs, suggesting features, or improving the docs - every bit helps.
137
+ We welcome everyone to contribute! Whether it's fixing bugs, suggesting features, or improving the docs. Every bit
138
+ helps.
116
139
 
117
- - Submit an issue
118
- - Open a pull request
119
- - Or just hop into our [Matrix](https://matrix.to/#/%23codesociety:matrix.org)
120
- or [Discord](https://discord.gg/code-society-823178343943897088) server and say hi!
140
+ - [Submit an issue](https://github.com/Code-Society-Lab/matrixpy/issues)
141
+ - [Open a pull request](https://github.com/Code-Society-Lab/matrixpy/blob/main/CONTRIBUTING.md)
142
+ - Hop into our [Matrix](https://matrix.to/#/%23codesociety:matrix.org)
143
+ or [Discord](https://discord.gg/code-society-823178343943897088) and say hi!
121
144
 
122
- If you intend to contribute, please read the [CONTRIBUTING.md](./CONTRIBUTING.md) first. Additionally, **every
123
- contributor** is expected to follow the [code of conduct](./CODE_OF_CONDUCT.md).
145
+ Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) and follow the [code of conduct](./CODE_OF_CONDUCT.md).
124
146
 
125
- # License
147
+ ## License
126
148
 
127
- This project is licensed under the terms
128
- of [MIT license](https://github.com/Code-Society-Lab/matrixpy/blob/main/LICENSE).
149
+ Released under the [MIT License](https://github.com/Code-Society-Lab/matrixpy/blob/main/LICENSE).
@@ -0,0 +1,25 @@
1
+ matrix/__init__.py,sha256=g8yEFjELnnwlvOKns-Ug6LgOezkjAFZ-Opt7esbBHKg,728
2
+ matrix/_version.py,sha256=mIVsMSXZUY9bR5xv4sLyWvbSaMCfZOJSgUtWBegbo6Q,530
3
+ matrix/bot.py,sha256=tbcn1Ra025I9plM-gf0NpzPKSTBlYe0_L8_RLO8w9GM,11984
4
+ matrix/checks.py,sha256=8geXrN8dEIR8HMbc2DQz4XFxLnBvjeqLUUqO9w1JPnA,678
5
+ matrix/command.py,sha256=I5sb9tTnhEd2RRfWRfGcJy3Xj0pA856KnHPm6JKPY7A,9942
6
+ matrix/config.py,sha256=JW_BBs-msIhtv1AGebZumLsx4td-Gp-NZaNYPJxRsEo,3680
7
+ matrix/content.py,sha256=z5_E2rTvHsODE52OiDkhDHNQAryx5NLhyHjBb65Xe-U,3853
8
+ matrix/context.py,sha256=tmtgzv3mtw4yNBw5ZUuDTr8fes1zWlaFZp7oU-WzAmk,3965
9
+ matrix/errors.py,sha256=HKGb5NUeFuZvieXgpLlVSmUxK4jpA0ODuiPQqQlbQTE,1676
10
+ matrix/extension.py,sha256=5f6pW_6D8wwN5qKhnQRkCWWDkkQw9t47j5XRNZNsyGQ,2399
11
+ matrix/group.py,sha256=P4NWNczfUpl0693lWswqafpEnYfzNXH_gz3swIlaxaE,3734
12
+ matrix/message.py,sha256=w6pu86goylxdrX5fgXPUMB_tW0bOFIk6tKy6qkXTjl4,5136
13
+ matrix/protocols.py,sha256=pIkHL4yuenvxh-gFhRPudCHJcfMBRxq2lUL5S5sPxpM,250
14
+ matrix/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ matrix/registry.py,sha256=OiEWaO66k4QOnOPCaXL5Ys-_TnVioI4q5ulyRP_fGS4,14202
16
+ matrix/room.py,sha256=PBuMWQo8mKy2d2XIeMbBlVBTnnqZjOPPGpKLp4K1AVM,14038
17
+ matrix/scheduler.py,sha256=8GDzxJhuDh32HRz3DyVw0SKXAQ9Az3t3jsDersSoB70,1597
18
+ matrix/types.py,sha256=UFjC7p8RAf7piEPvp2X3NuWdqBwkM9Yc3He7KWb9icc,384
19
+ matrix/help/__init__.py,sha256=1u7V7T_-VgYDeQCTXsc4y8Fo-8gJhOqYJq2U3cUjMWg,168
20
+ matrix/help/help_command.py,sha256=DEhOpMGx0-43m--sVqQjq-w71r6yKsrxSPDMFsr4oE4,12277
21
+ matrix/help/pagination.py,sha256=tmqgz_msP0qWefCiMDzDcGMP830z89VgQHxptj8ogzc,2175
22
+ matrix_python-1.4.10a0.dist-info/METADATA,sha256=YFYoLz8OIUYp28jtwVVyluQ5A6QwRRtMehpJzbwOnwk,6209
23
+ matrix_python-1.4.10a0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
24
+ matrix_python-1.4.10a0.dist-info/top_level.txt,sha256=BvHVM9c7-5SLzg-1OCRpHKgqAubWhRN1e38e6coHs-g,7
25
+ matrix_python-1.4.10a0.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- matrix/__init__.py,sha256=g8yEFjELnnwlvOKns-Ug6LgOezkjAFZ-Opt7esbBHKg,728
2
- matrix/_version.py,sha256=Sp0Aavn8Gz-jkQehzIA2xsa-Il-CUVey1i9m5oSJ344,528
3
- matrix/bot.py,sha256=tbcn1Ra025I9plM-gf0NpzPKSTBlYe0_L8_RLO8w9GM,11984
4
- matrix/checks.py,sha256=F_7432_OcFO-im4fRAj62MUsyv1mXywT4OsGC_7xbBQ,486
5
- matrix/command.py,sha256=GrP3WsT07sKehGX7PHfnT7gRX22d99877VPd0X2ViEw,10514
6
- matrix/config.py,sha256=JW_BBs-msIhtv1AGebZumLsx4td-Gp-NZaNYPJxRsEo,3680
7
- matrix/content.py,sha256=z5_E2rTvHsODE52OiDkhDHNQAryx5NLhyHjBb65Xe-U,3853
8
- matrix/context.py,sha256=-CbxY-LtK9-jgHERhvJH73B3SpO-Uk5ty0j1TMKfzuI,4032
9
- matrix/errors.py,sha256=HKGb5NUeFuZvieXgpLlVSmUxK4jpA0ODuiPQqQlbQTE,1676
10
- matrix/extension.py,sha256=RbCx58CdRXF8kGUgS-ec1aZdd-K5hQedhCCQ0-YR4Vg,2272
11
- matrix/group.py,sha256=TRIX7PE3lcB2ZWu3xY2W2OAmE_a8-i2zHNBYnX5uj28,3691
12
- matrix/message.py,sha256=w6pu86goylxdrX5fgXPUMB_tW0bOFIk6tKy6qkXTjl4,5136
13
- matrix/protocols.py,sha256=nFb4tLanwtrKWoIhZ96xMwXPjD3RF5ITca_yXtakXC4,166
14
- matrix/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- matrix/registry.py,sha256=PgdmbBADS-yqdFdj9x3QSsVfefx8Li848CU_UBbqCLQ,12646
16
- matrix/room.py,sha256=PBuMWQo8mKy2d2XIeMbBlVBTnnqZjOPPGpKLp4K1AVM,14038
17
- matrix/scheduler.py,sha256=EXsL9i8IDXhcpdW8lti0BR5XcIgkmud4iwOPaqcE9Gw,1727
18
- matrix/types.py,sha256=UFjC7p8RAf7piEPvp2X3NuWdqBwkM9Yc3He7KWb9icc,384
19
- matrix/help/__init__.py,sha256=1u7V7T_-VgYDeQCTXsc4y8Fo-8gJhOqYJq2U3cUjMWg,168
20
- matrix/help/help_command.py,sha256=xCLmKklw74LEMjbUfgQR9eaPMFvi3sPtDw2n2pnAnVQ,12800
21
- matrix/help/pagination.py,sha256=sJk0wC46sFHf7xl7WsGRAUc4FC7b9hPqmwQDmvcjwgM,2717
22
- matrix_python-1.4.8a0.dist-info/METADATA,sha256=Cw_QAKOVIvymFvEELdhYf72O0UkQOSk21KMpc01KAOY,5421
23
- matrix_python-1.4.8a0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
24
- matrix_python-1.4.8a0.dist-info/top_level.txt,sha256=BvHVM9c7-5SLzg-1OCRpHKgqAubWhRN1e38e6coHs-g,7
25
- matrix_python-1.4.8a0.dist-info/RECORD,,