matrix-python 0.1.2a0__tar.gz → 1.0.3a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. matrix_python-1.0.3a0/.github/workflows/codeql.yml +99 -0
  2. matrix_python-1.0.3a0/.github/workflows/scorecard.yml +52 -0
  3. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/.gitignore +1 -1
  4. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/PKG-INFO +3 -1
  5. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/README.md +2 -0
  6. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/scheduler.py +1 -2
  7. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/__init__.py +3 -1
  8. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/bot.py +41 -14
  9. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/checks.py +5 -2
  10. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/command.py +57 -38
  11. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/config.py +22 -12
  12. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/context.py +14 -3
  13. matrix_python-1.0.3a0/matrix/errors.py +57 -0
  14. matrix_python-1.0.3a0/matrix/group.py +66 -0
  15. matrix_python-1.0.3a0/matrix/help/__init__.py +4 -0
  16. matrix_python-1.0.3a0/matrix/help/help_command.py +376 -0
  17. matrix_python-1.0.3a0/matrix/help/pagination.py +92 -0
  18. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/message.py +5 -2
  19. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix_python.egg-info/PKG-INFO +3 -1
  20. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix_python.egg-info/SOURCES.txt +13 -3
  21. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/mypy.ini +2 -0
  22. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/pyproject.toml +1 -1
  23. matrix_python-1.0.3a0/tests/config_fixture.yaml +4 -0
  24. matrix_python-1.0.3a0/tests/config_fixture_token.yaml +1 -0
  25. matrix_python-1.0.3a0/tests/help/test_default_help_command.py +84 -0
  26. matrix_python-1.0.3a0/tests/help/test_help_command.py +174 -0
  27. matrix_python-1.0.3a0/tests/help/test_pagination.py +83 -0
  28. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/tests/test_bot.py +4 -24
  29. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/tests/test_context.py +1 -1
  30. matrix_python-1.0.3a0/tests/test_group.py +127 -0
  31. matrix_python-1.0.3a0/tests/test_help.py +0 -0
  32. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/tests/test_message.py +1 -1
  33. matrix_python-0.1.2a0/matrix/errors.py +0 -37
  34. matrix_python-0.1.2a0/matrix/help.py +0 -231
  35. matrix_python-0.1.2a0/tests/config_mixture.yaml +0 -4
  36. matrix_python-0.1.2a0/tests/test_help.py +0 -127
  37. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/.github/workflows/publish.yml +0 -0
  38. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/.github/workflows/tests.yml +0 -0
  39. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/CODE_OF_CONDUCT.md +0 -0
  40. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/CONTRIBUTING.md +0 -0
  41. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/LICENSE +0 -0
  42. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/checks.py +0 -0
  43. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/config.yaml +0 -0
  44. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/cooldown.py +0 -0
  45. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/error.py +0 -0
  46. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/ping.py +0 -0
  47. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/examples/reaction.py +0 -0
  48. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/room.py +0 -0
  49. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix/scheduler.py +0 -0
  50. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix_python.egg-info/dependency_links.txt +0 -0
  51. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix_python.egg-info/requires.txt +0 -0
  52. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/matrix_python.egg-info/top_level.txt +0 -0
  53. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/setup.cfg +0 -0
  54. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/tests/test_command.py +1 -1
  55. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/tests/test_config.py +0 -0
  56. {matrix_python-0.1.2a0 → matrix_python-1.0.3a0}/tests/test_room.py +0 -0
@@ -0,0 +1,99 @@
1
+ # For most projects, this workflow file will not need changing; you simply need
2
+ # to commit it to your repository.
3
+ #
4
+ # You may wish to alter this file to override the set of languages analyzed,
5
+ # or to provide custom queries or build logic.
6
+ #
7
+ # ******** NOTE ********
8
+ # We have attempted to detect the languages in your repository. Please check
9
+ # the `language` matrix defined below to confirm you have the correct set of
10
+ # supported CodeQL languages.
11
+ #
12
+ name: "CodeQL Advanced"
13
+
14
+ on:
15
+ push:
16
+ branches: [ "main" ]
17
+ pull_request:
18
+ branches: [ "main" ]
19
+ schedule:
20
+ - cron: '24 17 * * 4'
21
+
22
+ jobs:
23
+ analyze:
24
+ name: Analyze (${{ matrix.language }})
25
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
26
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
27
+ # - https://gh.io/supported-runners-and-hardware-resources
28
+ # - https://gh.io/using-larger-runners (GitHub.com only)
29
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31
+ permissions:
32
+ # required for all workflows
33
+ security-events: write
34
+
35
+ # required to fetch internal or private CodeQL packs
36
+ packages: read
37
+
38
+ # only required for workflows in private repositories
39
+ actions: read
40
+ contents: read
41
+
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ - language: python
47
+ build-mode: none
48
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
49
+ # Use `c-cpp` to analyze code written in C, C++ or both
50
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
51
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
52
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
53
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
54
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
55
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
56
+ steps:
57
+ - name: Checkout repository
58
+ uses: actions/checkout@v4
59
+
60
+ # Add any setup steps before running the `github/codeql-action/init` action.
61
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
62
+ # or others). This is typically only required for manual builds.
63
+ # - name: Setup runtime (example)
64
+ # uses: actions/setup-example@v1
65
+
66
+ # Initializes the CodeQL tools for scanning.
67
+ - name: Initialize CodeQL
68
+ uses: github/codeql-action/init@v4
69
+ with:
70
+ languages: ${{ matrix.language }}
71
+ build-mode: ${{ matrix.build-mode }}
72
+ # If you wish to specify custom queries, you can do so here or in a config file.
73
+ # By default, queries listed here will override any specified in a config file.
74
+ # Prefix the list here with "+" to use these queries and those in the config file.
75
+
76
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
77
+ # queries: security-extended,security-and-quality
78
+
79
+ # If the analyze step fails for one of the languages you are analyzing with
80
+ # "We were unable to automatically build your code", modify the matrix above
81
+ # to set the build mode to "manual" for that language. Then modify this step
82
+ # to build your code.
83
+ # ℹ️ Command-line programs to run using the OS shell.
84
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
85
+ - name: Run manual build steps
86
+ if: matrix.build-mode == 'manual'
87
+ shell: bash
88
+ run: |
89
+ echo 'If you are using a "manual" build mode for one or more of the' \
90
+ 'languages you are analyzing, replace this with the commands to build' \
91
+ 'your code, for example:'
92
+ echo ' make bootstrap'
93
+ echo ' make release'
94
+ exit 1
95
+
96
+ - name: Perform CodeQL Analysis
97
+ uses: github/codeql-action/analyze@v4
98
+ with:
99
+ category: "/language:${{matrix.language}}"
@@ -0,0 +1,52 @@
1
+ name: Scorecard supply-chain security
2
+ on:
3
+ branch_protection_rule:
4
+ schedule:
5
+ - cron: '16 0 * * 4'
6
+ push:
7
+ branches: ["main"]
8
+ workflow_dispatch:
9
+
10
+ permissions: read-all
11
+
12
+ jobs:
13
+ analysis:
14
+ name: Scorecard analysis
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ # Needed to upload the results to code-scanning dashboard.
18
+ security-events: write
19
+ # Needed to publish results and get a badge (see publish_results below).
20
+ id-token: write
21
+ contents: read
22
+ actions: read
23
+
24
+ steps:
25
+ - name: Harden Runner
26
+ uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
27
+ with:
28
+ egress-policy: audit
29
+
30
+ - name: "Checkout code"
31
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - name: "Run analysis"
36
+ uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
37
+ with:
38
+ results_file: results.sarif
39
+ results_format: sarif
40
+ publish_results: true
41
+
42
+ - name: "Upload artifact"
43
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
44
+ with:
45
+ name: SARIF file
46
+ path: results.sarif
47
+ retention-days: 5
48
+
49
+ - name: "Upload to code-scanning"
50
+ uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
51
+ with:
52
+ sarif_file: results.sarif
@@ -165,7 +165,7 @@ cython_debug/
165
165
  # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
166
  # and can be added to the global gitignore or merged into this file. For a more nuclear
167
167
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
- #.idea/
168
+ .idea/
169
169
 
170
170
  # Ruff stuff:
171
171
  .ruff_cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 0.1.2a0
3
+ Version: 1.0.3a0
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>
@@ -710,6 +710,8 @@ Requires-Dist: types-Markdown; extra == "dev"
710
710
  [![Static Badge](https://img.shields.io/badge/%F0%9F%93%9A-Documentation-%235c5c5c)](https://github.com/Code-Society-Lab/matrixpy/wiki)
711
711
  [![Join on Discord](https://discordapp.com/api/guilds/823178343943897088/widget.png?style=shield)](https://discord.gg/code-society-823178343943897088)
712
712
  [![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)
713
+ [![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)
714
+ [![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)
713
715
 
714
716
  Matrix.py is a lightweight and intuitive Python library to build bots on
715
717
  the [Matrix protocol]([Matrix](https://matrix.org)). It provides a clean,
@@ -11,6 +11,8 @@
11
11
  [![Static Badge](https://img.shields.io/badge/%F0%9F%93%9A-Documentation-%235c5c5c)](https://github.com/Code-Society-Lab/matrixpy/wiki)
12
12
  [![Join on Discord](https://discordapp.com/api/guilds/823178343943897088/widget.png?style=shield)](https://discord.gg/code-society-823178343943897088)
13
13
  [![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)
14
+ [![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)
15
+ [![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)
14
16
 
15
17
  Matrix.py is a lightweight and intuitive Python library to build bots on
16
18
  the [Matrix protocol]([Matrix](https://matrix.org)). It provides a clean,
@@ -1,7 +1,6 @@
1
1
  from matrix import Bot, Context
2
- from matrix.message import Message
3
2
 
4
- bot = Bot("config.yaml")
3
+ bot = Bot("examples/config.yaml")
5
4
 
6
5
  room_id = "!your_room_id:matrix.org" # Replace with your room ID
7
6
 
@@ -1,8 +1,9 @@
1
1
  """A simple, developer-friendly library to create powerful Matrix bots."""
2
2
 
3
- __version__ = "0.1.2-alpha"
3
+ __version__ = "1.0.3-alpha"
4
4
 
5
5
  from .bot import Bot
6
+ from .group import Group
6
7
  from .config import Config
7
8
  from .context import Context
8
9
  from .command import Command
@@ -11,6 +12,7 @@ from .checks import cooldown
11
12
 
12
13
  __all__ = [
13
14
  "Bot",
15
+ "Group",
14
16
  "Config",
15
17
  "Command",
16
18
  "Context",
@@ -25,15 +25,22 @@ from nio import (
25
25
  )
26
26
 
27
27
  from .room import Room
28
+ from .group import Group
28
29
  from .config import Config
29
30
  from .context import Context
30
31
  from .command import Command
31
- from .help import HelpCommand
32
- from .errors import AlreadyRegisteredError, CommandNotFoundError, CheckError
32
+ from .help import HelpCommand, DefaultHelpCommand
33
33
  from .scheduler import Scheduler
34
34
 
35
+ from .errors import (
36
+ AlreadyRegisteredError,
37
+ CommandNotFoundError,
38
+ CheckError,
39
+ )
40
+
35
41
 
36
42
  Callback = Callable[..., Coroutine[Any, Any, Any]]
43
+ GroupCallable = Callable[[Callable[..., Coroutine[Any, Any, Any]]], Group]
37
44
  ErrorCallback = Callable[[Exception], Coroutine]
38
45
  CommandErrorCallback = Callable[["Context", Exception], Coroutine[Any, Any, Any]]
39
46
 
@@ -66,13 +73,15 @@ class Bot:
66
73
  "on_member_change": RoomMemberEvent,
67
74
  }
68
75
 
69
- def __init__(self, config: Optional[Union[Config, str]] = None, **kwargs) -> None:
76
+ def __init__(
77
+ self, *, config: Union[Config, str], help: Optional[HelpCommand] = None
78
+ ) -> None:
70
79
  if isinstance(config, Config):
71
80
  self.config = config
72
81
  elif isinstance(config, str):
73
82
  self.config = Config(config_path=config)
74
83
  else:
75
- self.config = Config(**kwargs)
84
+ raise TypeError("config must be a Config instance or a config file path")
76
85
 
77
86
  self.client: AsyncClient = AsyncClient(self.config.homeserver)
78
87
  self.log: logging.Logger = logging.getLogger(__name__)
@@ -89,7 +98,7 @@ class Bot:
89
98
  self._error_handlers: dict[type[Exception], ErrorCallback] = {}
90
99
  self._command_error_handlers: dict[type[Exception], CommandErrorCallback] = {}
91
100
 
92
- self.help: HelpCommand = kwargs.get("help", HelpCommand(prefix=self.prefix))
101
+ self.help: HelpCommand = help or DefaultHelpCommand(prefix=self.prefix)
93
102
  self.register_command(self.help)
94
103
 
95
104
  self.client.add_event_callback(self._on_event, Event)
@@ -179,7 +188,14 @@ class Bot:
179
188
  return wrapper(func)
180
189
 
181
190
  def command(
182
- self, name: Optional[str] = None, cooldown: Optional[tuple[int, float]] = None
191
+ self,
192
+ name: Optional[str] = None,
193
+ *,
194
+ description: Optional[str] = None,
195
+ prefix: Optional[str] = None,
196
+ parent: Optional[str] = None,
197
+ usage: Optional[str] = None,
198
+ cooldown: Optional[tuple[int, float]] = None,
183
199
  ) -> Callable[[Callback], Command]:
184
200
  """
185
201
  Decorator to register a coroutine function as a command handler.
@@ -187,9 +203,12 @@ class Bot:
187
203
  The command name defaults to the function name unless
188
204
  explicitly provided.
189
205
 
190
- :param name: The name of the command. If omitted, the function
191
- name is used.
192
- :type name: str, optional
206
+ :param name: The name of the command. If omitted, the function name is used.
207
+ :param description: A brief description of the command.
208
+ :param prefix: The command prefix. If omitted, the bot's default prefix is used.
209
+ :param parent: The parent command name for subcommands.
210
+ :param usage: A usage string describing command arguments.
211
+ :param cooldown: A tuple defining (max_calls, per_seconds) for rate limiting.
193
212
  :raises TypeError: If the decorated function is not a coroutine.
194
213
  :raises ValueError: If a command with the same name is registered.
195
214
  :return: Decorator that registers the command handler.
@@ -197,12 +216,20 @@ class Bot:
197
216
  """
198
217
 
199
218
  def wrapper(func: Callback) -> Command:
200
- cmd = Command(func, name=name, cooldown=cooldown, prefix=self.prefix)
219
+ cmd = Command(
220
+ func,
221
+ name=name,
222
+ description=description,
223
+ prefix=prefix,
224
+ parent=parent,
225
+ usage=usage,
226
+ cooldown=cooldown,
227
+ )
201
228
  return self.register_command(cmd)
202
229
 
203
230
  return wrapper
204
231
 
205
- def schedule(self, cron: str):
232
+ def schedule(self, cron: str) -> Callable[..., Callback]:
206
233
  """
207
234
  Decorator to register a coroutine function as a scheduled task.
208
235
 
@@ -225,12 +252,12 @@ class Bot:
225
252
 
226
253
  return wrapper
227
254
 
228
- def register_command(self, cmd: Command):
255
+ def register_command(self, cmd: Command) -> Command:
229
256
  if cmd in self.commands:
230
257
  raise AlreadyRegisteredError(cmd)
231
258
 
232
259
  self.commands[cmd.name] = cmd
233
- self.log.debug("command %s registered", cmd)
260
+ self.log.debug("command '%s' registered", cmd)
234
261
 
235
262
  return cmd
236
263
 
@@ -312,7 +339,7 @@ class Bot:
312
339
 
313
340
  await ctx.command(ctx)
314
341
 
315
- async def _build_context(self, room: MatrixRoom, event: Event):
342
+ async def _build_context(self, room: MatrixRoom, event: Event) -> Context:
316
343
  """Builds the base context and extracts the command from the event"""
317
344
  ctx = Context(bot=self, room=room, event=event)
318
345
 
@@ -1,4 +1,7 @@
1
- from typing import Callable
1
+ from typing import TYPE_CHECKING, Callable
2
+
3
+ if TYPE_CHECKING:
4
+ from .command import Command
2
5
 
3
6
 
4
7
  def cooldown(rate: int, period: float) -> Callable:
@@ -11,7 +14,7 @@ def cooldown(rate: int, period: float) -> Callable:
11
14
  :type period: float
12
15
  """
13
16
 
14
- def wrapper(cmd):
17
+ def wrapper(cmd: Command) -> Command:
15
18
  cmd.set_cooldown(rate, period)
16
19
  return cmd
17
20
 
@@ -31,32 +31,43 @@ class Command:
31
31
  :param func: The coroutine that is executed when the command is invoked.
32
32
  :type func: Callable[..., Coroutine[Any, Any, Any]]
33
33
 
34
- :keyword str name: Optional name. Defaults to the function's name.
35
- :keyword str help: Optional help text displayed to users.
36
- :keyword str usage: Optional usage string for the command.
37
- :keyword str description: Optional description of what the command does.
34
+ :param name: Optional name. Defaults to the function's name.
35
+ :param description: Optional description of what the command does.
36
+ :param prefix: Optional prefix for the command.
37
+ :param parent: Optional parent command name for subcommands.
38
+ :param usage: Optional usage string for the command.
39
+ :param cooldown: Optional cooldown settings as a tuple of (rate, period).
38
40
 
39
41
  :raises TypeError: If the provided name is not a string.
40
42
  :raises TypeError: If the provided callback is not a coroutine.
41
43
  """
42
44
 
43
- def __init__(self, func: Callback, **kwargs: Any):
44
- name: str = kwargs.get("name") or func.__name__
45
-
46
- if not isinstance(name, str):
45
+ def __init__(
46
+ self,
47
+ func: Callback,
48
+ *,
49
+ name: Optional[str] = None,
50
+ description: Optional[str] = None,
51
+ prefix: Optional[str] = None,
52
+ parent: Optional[str] = None,
53
+ usage: Optional[str] = None,
54
+ cooldown: Optional[tuple[int, float]] = None,
55
+ ):
56
+ if name is not None and not isinstance(name, str):
47
57
  raise TypeError("Name must be a string.")
48
58
 
49
- self.name: str = name
59
+ self.name: str = name or func.__name__
50
60
  self.callback = func
51
61
  self.checks: List[Callback] = []
52
62
 
53
- self.description: str = kwargs.get("description", "")
54
- self.prefix: str = kwargs.get("prefix", "")
55
- self.usage: str = kwargs.get("usage", self._build_usage())
63
+ self.description: str = description or ""
64
+ self.prefix: str = prefix or ""
65
+ self.parent: str = parent or ""
66
+ self.usage: str = usage or self._build_usage()
56
67
  self.help: str = self._build_help()
57
68
 
58
- self._before_invoke: Optional[Callback] = None
59
- self._after_invoke: Optional[Callback] = None
69
+ self._before_invoke_callback: Optional[Callback] = None
70
+ self._after_invoke_callback: Optional[Callback] = None
60
71
  self._on_error: Optional[ErrorCallback] = None
61
72
  self._error_handlers: dict[type[Exception], ErrorCallback] = {}
62
73
 
@@ -64,7 +75,7 @@ class Command:
64
75
  self.cooldown_period: Optional[float] = None
65
76
  self.cooldown_calls: DefaultDict[str, deque[float]] = defaultdict(deque)
66
77
 
67
- if cooldown := kwargs.get("cooldown"):
78
+ if cooldown:
68
79
  self.set_cooldown(*cooldown)
69
80
 
70
81
  @property
@@ -115,7 +126,12 @@ class Command:
115
126
  :rtype: str
116
127
  """
117
128
  params = " ".join(f"[{p.name}]" for p in self.params)
118
- return f"{self.prefix}{self.name} {params}"
129
+ command_name = self.name
130
+
131
+ if self.parent:
132
+ command_name = f"{self.parent} {self.name}"
133
+
134
+ return f"{self.prefix}{command_name} {params}"
119
135
 
120
136
  def _parse_arguments(self, ctx: "Context") -> list[Any]:
121
137
  parsed_args = []
@@ -153,10 +169,13 @@ class Command:
153
169
  self.cooldown_rate = rate
154
170
  self.cooldown_period = period
155
171
 
156
- async def cooldown_function(ctx):
172
+ async def cooldown_function(ctx: "Context") -> bool:
157
173
  if ctx is None or not hasattr(ctx, "sender"):
158
174
  return False
159
175
 
176
+ if self.cooldown_period is None or self.cooldown_rate is None:
177
+ return False
178
+
160
179
  now = monotonic()
161
180
  user_id = ctx.sender
162
181
  calls = self.cooldown_calls[user_id]
@@ -186,7 +205,7 @@ class Command:
186
205
  if not asyncio.iscoroutinefunction(func):
187
206
  raise TypeError("The hook must be a coroutine.")
188
207
 
189
- self._before_invoke = func
208
+ self._before_invoke_callback = func
190
209
 
191
210
  def after_invoke(self, func: Callback) -> None:
192
211
  """
@@ -201,7 +220,7 @@ class Command:
201
220
  if not asyncio.iscoroutinefunction(func):
202
221
  raise TypeError("The hook must be a coroutine.")
203
222
 
204
- self._after_invoke = func
223
+ self._after_invoke_callback = func
205
224
 
206
225
  def error(self, exception: Optional[type[Exception]] = None) -> Callable:
207
226
  """
@@ -250,17 +269,25 @@ class Command:
250
269
  ctx.logger.exception("error while executing command '%s'", self)
251
270
  raise error
252
271
 
253
- async def __before_invoke(self, ctx: "Context") -> None:
254
- for check in self.checks:
255
- if not await check(ctx):
256
- raise CheckError(self, check)
272
+ async def invoke(self, ctx: "Context") -> None:
273
+ parsed_args = self._parse_arguments(ctx)
274
+ await self.callback(ctx, *parsed_args)
275
+
276
+ async def _invoke(self, ctx: "Context") -> None:
277
+ try:
278
+ for check in self.checks:
279
+ if not await check(ctx):
280
+ raise CheckError(self, check)
257
281
 
258
- if self._before_invoke:
259
- await self._before_invoke(ctx)
282
+ if self._before_invoke_callback:
283
+ await self._before_invoke_callback(ctx)
260
284
 
261
- async def __after_invoke(self, ctx: "Context") -> None:
262
- if self._after_invoke:
263
- await self._after_invoke(ctx)
285
+ await self.invoke(ctx)
286
+
287
+ if self._after_invoke_callback:
288
+ await self._after_invoke_callback(ctx)
289
+ except Exception as error:
290
+ await self.on_error(ctx, error)
264
291
 
265
292
  async def __call__(self, ctx: "Context") -> None:
266
293
  """
@@ -269,17 +296,9 @@ class Command:
269
296
  :param ctx: The command execution context.
270
297
  :type ctx: Context
271
298
  """
272
- try:
273
- await self.__before_invoke(ctx)
274
-
275
- parsed_args = self._parse_arguments(ctx)
276
- await self.callback(ctx, *parsed_args)
277
-
278
- await self.__after_invoke(ctx)
279
- except Exception as error:
280
- await self.on_error(ctx, error)
299
+ await self._invoke(ctx)
281
300
 
282
- def __eq__(self, other) -> bool:
301
+ def __eq__(self, other: object) -> bool:
283
302
  return self.name == other
284
303
 
285
304
  def __hash__(self) -> int:
@@ -13,30 +13,40 @@ class Config:
13
13
  token: (Optional) One of the password or token must be provided.
14
14
  prefix: Defaults to '!' if not specified in the config file.
15
15
 
16
- :param config_path: (Optional) Path to the YAML configuration file.
17
- :type config_path: str
18
-
19
- :param **kwargs: (Optional) Varaiable to Matrix client configuration.
20
- :type **kwargs: dict[str, Any]
16
+ :param config_path: Path to the YAML configuration file.
17
+ :param homeserver: The Matrix homeserver URL.
18
+ :param username: The Matrix user ID (username).
19
+ :param password: The password for the Matrix user.
20
+ :param token: The access token for the Matrix user.
21
+ :param prefix: The command prefix.
21
22
 
22
23
  :raises FileNotFoundError: If the configuration file does not exist.
23
24
  :raises yaml.YAMLError: If the configuration file cannot be parsed.
24
25
  :raises ConfigError: If neither password or token has been provided.
25
26
  """
26
27
 
27
- def __init__(self, config_path: Optional[str] = None, **kwargs):
28
- self.homeserver: str = kwargs.get("homeserver", "https://matrix.org")
29
- self.user_id: Optional[str] = kwargs.get("username")
30
- self.password: Optional[str] = kwargs.get("password", None)
31
- self.token: Optional[str] = kwargs.get("token", None)
32
- self.prefix: str = kwargs.get("prefix", "!")
28
+ def __init__(
29
+ self,
30
+ config_path: Optional[str] = None,
31
+ *,
32
+ homeserver: Optional[str] = None,
33
+ username: Optional[str] = None,
34
+ password: Optional[str] = None,
35
+ token: Optional[str] = None,
36
+ prefix: Optional[str] = None,
37
+ ) -> None:
38
+ self.homeserver: str = homeserver or "https://matrix.org"
39
+ self.user_id: Optional[str] = username
40
+ self.password: Optional[str] = password
41
+ self.token: Optional[str] = token
42
+ self.prefix: str = prefix or "!"
33
43
 
34
44
  if config_path:
35
45
  self.load_from_file(config_path)
36
46
  elif not (self.password or self.token):
37
47
  raise ConfigError("username and password or token")
38
48
 
39
- def load_from_file(self, config_path: str):
49
+ def load_from_file(self, config_path: str) -> None:
40
50
  """Load Matrix client settings via YAML config file."""
41
51
  with open(config_path, "r") as f:
42
52
  config = yaml.safe_load(f)
@@ -42,6 +42,7 @@ class Context:
42
42
  # Command metdata
43
43
  self.prefix: str = bot.prefix
44
44
  self.command: Optional[Command] = None
45
+ self.subcommand: Optional[Command] = None
45
46
  self._args: List[str] = shlex.split(self.body)
46
47
 
47
48
  @property
@@ -54,8 +55,12 @@ class Context:
54
55
  :return: The list of arguments.
55
56
  :rtype: List[str]
56
57
  """
58
+ if self.subcommand:
59
+ return self._args[2:]
60
+
57
61
  if self.command:
58
62
  return self._args[1:]
63
+
59
64
  return self._args
60
65
 
61
66
  @property
@@ -80,6 +85,12 @@ class Context:
80
85
  raise MatrixError(f"Failed to send message: {e}")
81
86
 
82
87
  async def send_help(self) -> None:
83
- if not self.command:
84
- return await self.bot.help.execute(self)
85
- await self.reply(self.command.help)
88
+ if self.subcommand:
89
+ await self.reply(self.subcommand.help)
90
+ return
91
+
92
+ if self.command:
93
+ await self.reply(self.command.help)
94
+ return
95
+
96
+ await self.bot.help.execute(self)