matrix-python 1.4.4a0__tar.gz → 1.4.6a0__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 (66) hide show
  1. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/PKG-INFO +12 -11
  2. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/_version.py +3 -3
  3. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/bot.py +2 -3
  4. matrix_python-1.4.6a0/matrix/config.py +125 -0
  5. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/PKG-INFO +12 -11
  6. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/SOURCES.txt +0 -2
  7. matrix_python-1.4.6a0/matrix_python.egg-info/requires.txt +14 -0
  8. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/pyproject.toml +15 -14
  9. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_bot.py +49 -18
  10. matrix_python-1.4.6a0/tests/test_config.py +139 -0
  11. matrix_python-1.4.4a0/matrix/config.py +0 -61
  12. matrix_python-1.4.4a0/matrix_python.egg-info/requires.txt +0 -13
  13. matrix_python-1.4.4a0/tests/config_fixture.yaml +0 -4
  14. matrix_python-1.4.4a0/tests/config_fixture_token.yaml +0 -1
  15. matrix_python-1.4.4a0/tests/test_config.py +0 -81
  16. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.github/dependabot.yml +0 -0
  17. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.github/workflows/CODEOWNERS +0 -0
  18. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.github/workflows/codeql.yml +0 -0
  19. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.github/workflows/publish.yml +0 -0
  20. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.github/workflows/scorecard.yml +0 -0
  21. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.github/workflows/tests.yml +0 -0
  22. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/.gitignore +0 -0
  23. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/CODE_OF_CONDUCT.md +0 -0
  24. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/CONTRIBUTING.md +0 -0
  25. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/LICENSE +0 -0
  26. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/README.md +0 -0
  27. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/README.md +0 -0
  28. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/checks.py +0 -0
  29. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/config.yaml +0 -0
  30. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/cooldown.py +0 -0
  31. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/error_handling.py +0 -0
  32. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/extension.py +0 -0
  33. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/ping.py +0 -0
  34. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/reaction.py +0 -0
  35. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/examples/scheduler.py +0 -0
  36. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/__init__.py +0 -0
  37. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/checks.py +0 -0
  38. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/command.py +0 -0
  39. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/content.py +0 -0
  40. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/context.py +0 -0
  41. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/errors.py +0 -0
  42. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/extension.py +0 -0
  43. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/group.py +0 -0
  44. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/help/__init__.py +0 -0
  45. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/help/help_command.py +0 -0
  46. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/help/pagination.py +0 -0
  47. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/message.py +0 -0
  48. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/protocols.py +0 -0
  49. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/registry.py +0 -0
  50. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/room.py +0 -0
  51. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/scheduler.py +0 -0
  52. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix/types.py +0 -0
  53. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/dependency_links.txt +0 -0
  54. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/top_level.txt +0 -0
  55. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/mypy.ini +0 -0
  56. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/setup.cfg +0 -0
  57. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/help/test_default_help_command.py +0 -0
  58. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/help/test_help_command.py +0 -0
  59. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/help/test_pagination.py +0 -0
  60. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_command.py +0 -0
  61. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_context.py +0 -0
  62. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_extension.py +0 -0
  63. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_group.py +0 -0
  64. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_message.py +0 -0
  65. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_registry.py +0 -0
  66. {matrix_python-1.4.4a0 → matrix_python-1.4.6a0}/tests/test_room.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.4a0
3
+ Version: 1.4.6a0
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>
@@ -684,18 +684,19 @@ Project-URL: Source, https://github.com/Code-Society-Lab/matrixpy
684
684
  Project-URL: Issues, https://github.com/Code-Society-Lab/matrixpy/issues
685
685
  Requires-Python: >=3.10
686
686
  Description-Content-Type: text/markdown
687
- Requires-Dist: matrix-nio
687
+ Requires-Dist: matrix-nio==0.25.2
688
688
  Requires-Dist: logger
689
- Requires-Dist: PyYAML
690
- Requires-Dist: markdown
691
- Requires-Dist: APScheduler
689
+ Requires-Dist: PyYAML==6.0.3
690
+ Requires-Dist: markdown==3.10.2
691
+ Requires-Dist: APScheduler==3.11.2
692
+ Requires-Dist: envyaml==1.10.211231
692
693
  Provides-Extra: dev
693
- Requires-Dist: pytest; extra == "dev"
694
- Requires-Dist: pytest-asyncio; extra == "dev"
695
- Requires-Dist: black; extra == "dev"
696
- Requires-Dist: mypy; extra == "dev"
697
- Requires-Dist: types-PyYAML; extra == "dev"
698
- Requires-Dist: types-Markdown; extra == "dev"
694
+ Requires-Dist: pytest==9.0.3; extra == "dev"
695
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
696
+ Requires-Dist: black==26.3.1; extra == "dev"
697
+ Requires-Dist: mypy==1.20.0; extra == "dev"
698
+ Requires-Dist: types-PyYAML==6.0.12.20260408; extra == "dev"
699
+ Requires-Dist: types-Markdown==3.10.2.20260408; extra == "dev"
699
700
 
700
701
  <div align="center">
701
702
  <em>A simple, developer-friendly library to create powerful <a href="https://matrix.org">Matrix</a> bots.</em>
@@ -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.4a0'
22
- __version_tuple__ = version_tuple = (1, 4, 4, 'a0')
21
+ __version__ = version = '1.4.6a0'
22
+ __version_tuple__ = version_tuple = (1, 4, 6, 'a0')
23
23
 
24
- __commit_id__ = commit_id = 'gaaee53bc7'
24
+ __commit_id__ = commit_id = 'g9d2b531d5'
@@ -245,8 +245,7 @@ class Bot(Registry):
245
245
  :func:`asyncio.run`, and ensures the client is closed gracefully
246
246
  on interruption.
247
247
  """
248
- if config is not None:
249
- self._load_config(config)
248
+ self._load_config(config)
250
249
 
251
250
  try:
252
251
  asyncio.run(self.run())
@@ -264,7 +263,7 @@ class Bot(Registry):
264
263
  calls the :meth:`on_ready` hook, and starts the long-running
265
264
  sync loop for receiving events.
266
265
  """
267
- self.client.user = self.config.user_id
266
+ self.client.user = self.config.username
268
267
 
269
268
  self.start_at = time.time()
270
269
  self.log.info("starting – timestamp=%s", self.start_at)
@@ -0,0 +1,125 @@
1
+ from typing import Any
2
+
3
+ from envyaml import EnvYAML
4
+
5
+ from .errors import ConfigError
6
+
7
+
8
+ class Config:
9
+ """Configuration handler for Matrix client settings.
10
+
11
+ Manages all settings required to connect and authenticate with a Matrix
12
+ homeserver. Configuration can be loaded from a YAML file or provided
13
+ directly via constructor parameters. At least one authentication method
14
+ must be provided.
15
+
16
+ # Example
17
+
18
+ ```python
19
+ # Load from file
20
+ config = Config(config_path="path/to/config..yaml")
21
+
22
+ # Manual configuration
23
+ config = Config(username="@bot:matrix.org", password="secret")
24
+ ```
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ config_path: str | None = None,
30
+ *,
31
+ homeserver: str | None = None,
32
+ username: str | None = None,
33
+ password: str | None = None,
34
+ token: str | None = None,
35
+ prefix: str | None = None,
36
+ ) -> None:
37
+ """Initialize the bot configuration.
38
+
39
+ Loads configuration from a YAML file if provided, otherwise uses
40
+ the provided parameters directly. At least one of password or token
41
+ must be supplied.
42
+
43
+ # Example
44
+
45
+ ```python
46
+ config = Config(
47
+ username="@bot:matrix.org",
48
+ password="secret",
49
+ prefix="!",
50
+ )
51
+ ```
52
+ """
53
+ self._data: dict[str, Any] = {}
54
+
55
+ self.homeserver: str = homeserver or "https://matrix.org"
56
+ self.username: str | None = username
57
+ self.password: str | None = password
58
+ self.token: str | None = token
59
+ self.prefix: str = prefix or "!"
60
+
61
+ if config_path:
62
+ self.load_from_file(config_path)
63
+ else:
64
+ if not self.password and not self.token:
65
+ raise ConfigError("username and password or token")
66
+
67
+ self._data = {
68
+ "HOMESERVER": self.homeserver,
69
+ "USERNAME": self.username,
70
+ "PASSWORD": self.password,
71
+ "TOKEN": self.token,
72
+ "PREFIX": self.prefix,
73
+ }
74
+
75
+ def load_from_file(self, config_path: str) -> None:
76
+ """Load Matrix client settings from a YAML config file.
77
+
78
+ Supports environment variable substitution via EnvYAML. Values in
79
+ the YAML file can reference environment variables using ${VAR} syntax.
80
+
81
+ # Example
82
+
83
+ ```python
84
+ config = Config()
85
+ config.load_from_file("path/to/config.yaml")
86
+ ```
87
+ """
88
+ self._data = dict(EnvYAML(config_path))
89
+
90
+ password = self._data.get("PASSWORD", None)
91
+ token = self._data.get("TOKEN", None)
92
+
93
+ if not password and not token:
94
+ raise ConfigError("USERNAME and PASSWORD or TOKEN")
95
+
96
+ self.homeserver = self._data.get("HOMESERVER", "https://matrix.org")
97
+ self.username = self._data.get("USERNAME")
98
+ self.password = password
99
+ self.token = token
100
+ self.prefix = self._data.get("PREFIX", "!")
101
+
102
+ def get(self, key: str, *, section: str | None = None, default: Any = None) -> Any:
103
+ """Access a config value by key, optionally scoped to a section.
104
+
105
+ # Example
106
+
107
+ ```python
108
+ config.get(key="main_channel", section="bot")
109
+ config.get(key="log_level", default="INFO")
110
+ ```
111
+ """
112
+ if section in self._data:
113
+ return self._data.get(section, {}).get(key, default)
114
+ return self._data.get(key, default)
115
+
116
+ def __getitem__(self, key: str) -> Any:
117
+ """Access a config value by key, raising KeyError if not found.
118
+
119
+ # Example
120
+
121
+ ```python
122
+ config["bot"]["main_channel"]
123
+ ```
124
+ """
125
+ return self._data[key]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrix-python
3
- Version: 1.4.4a0
3
+ Version: 1.4.6a0
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>
@@ -684,18 +684,19 @@ Project-URL: Source, https://github.com/Code-Society-Lab/matrixpy
684
684
  Project-URL: Issues, https://github.com/Code-Society-Lab/matrixpy/issues
685
685
  Requires-Python: >=3.10
686
686
  Description-Content-Type: text/markdown
687
- Requires-Dist: matrix-nio
687
+ Requires-Dist: matrix-nio==0.25.2
688
688
  Requires-Dist: logger
689
- Requires-Dist: PyYAML
690
- Requires-Dist: markdown
691
- Requires-Dist: APScheduler
689
+ Requires-Dist: PyYAML==6.0.3
690
+ Requires-Dist: markdown==3.10.2
691
+ Requires-Dist: APScheduler==3.11.2
692
+ Requires-Dist: envyaml==1.10.211231
692
693
  Provides-Extra: dev
693
- Requires-Dist: pytest; extra == "dev"
694
- Requires-Dist: pytest-asyncio; extra == "dev"
695
- Requires-Dist: black; extra == "dev"
696
- Requires-Dist: mypy; extra == "dev"
697
- Requires-Dist: types-PyYAML; extra == "dev"
698
- Requires-Dist: types-Markdown; extra == "dev"
694
+ Requires-Dist: pytest==9.0.3; extra == "dev"
695
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
696
+ Requires-Dist: black==26.3.1; extra == "dev"
697
+ Requires-Dist: mypy==1.20.0; extra == "dev"
698
+ Requires-Dist: types-PyYAML==6.0.12.20260408; extra == "dev"
699
+ Requires-Dist: types-Markdown==3.10.2.20260408; extra == "dev"
699
700
 
700
701
  <div align="center">
701
702
  <em>A simple, developer-friendly library to create powerful <a href="https://matrix.org">Matrix</a> bots.</em>
@@ -45,8 +45,6 @@ matrix_python.egg-info/SOURCES.txt
45
45
  matrix_python.egg-info/dependency_links.txt
46
46
  matrix_python.egg-info/requires.txt
47
47
  matrix_python.egg-info/top_level.txt
48
- tests/config_fixture.yaml
49
- tests/config_fixture_token.yaml
50
48
  tests/test_bot.py
51
49
  tests/test_command.py
52
50
  tests/test_config.py
@@ -0,0 +1,14 @@
1
+ matrix-nio==0.25.2
2
+ logger
3
+ PyYAML==6.0.3
4
+ markdown==3.10.2
5
+ APScheduler==3.11.2
6
+ envyaml==1.10.211231
7
+
8
+ [dev]
9
+ pytest==9.0.3
10
+ pytest-asyncio==1.3.0
11
+ black==26.3.1
12
+ mypy==1.20.0
13
+ types-PyYAML==6.0.12.20260408
14
+ types-Markdown==3.10.2.20260408
@@ -5,33 +5,34 @@ requires = ["setuptools>=64", "setuptools_scm>=8"]
5
5
  name = "matrix-python"
6
6
  dynamic = ["version"]
7
7
  authors = [
8
- { name="Simon Roy" },
9
- { name=" Chris Dedman Rollet " }
8
+ { name = "Simon Roy" },
9
+ { name = " Chris Dedman Rollet " }
10
10
  ]
11
11
  maintainers = [
12
- { name="Code Society Lab", email="admin@codesociety.xyz" }
12
+ { name = "Code Society Lab", email = "admin@codesociety.xyz" }
13
13
  ]
14
14
  description = "An easy-to-use Matrix bot framework designed for quick development and minimal setup"
15
15
  readme = "README.md"
16
- license = { file="LICENSE" }
16
+ license = { file = "LICENSE" }
17
17
  requires-python = ">=3.10"
18
18
 
19
19
  dependencies = [
20
- "matrix-nio",
20
+ "matrix-nio==0.25.2",
21
21
  "logger",
22
- 'PyYAML',
23
- 'markdown',
24
- 'APScheduler',
22
+ "PyYAML==6.0.3",
23
+ "markdown==3.10.2",
24
+ "APScheduler==3.11.2",
25
+ "envyaml==1.10.211231",
25
26
  ]
26
27
 
27
28
  [project.optional-dependencies]
28
29
  dev = [
29
- "pytest",
30
- "pytest-asyncio",
31
- "black",
32
- "mypy",
33
- "types-PyYAML",
34
- 'types-Markdown',
30
+ "pytest==9.0.3",
31
+ "pytest-asyncio==1.3.0",
32
+ "black==26.3.1",
33
+ "mypy==1.20.0",
34
+ "types-PyYAML==6.0.12.20260408",
35
+ "types-Markdown==3.10.2.20260408",
35
36
  ]
36
37
 
37
38
  [project.urls]
@@ -16,7 +16,30 @@ from matrix.errors import (
16
16
  @pytest.fixture
17
17
  def bot():
18
18
  b = Bot()
19
- b._load_config("tests/config_fixture.yaml")
19
+ b._load_config(
20
+ Config(
21
+ username="grace",
22
+ password="grace1234",
23
+ )
24
+ )
25
+
26
+ b._client = MagicMock()
27
+ b._client.room_send = AsyncMock()
28
+ b.log = MagicMock()
29
+ b.log.getChild.return_value = MagicMock()
30
+
31
+ return b
32
+
33
+
34
+ @pytest.fixture
35
+ def bot_with_token():
36
+ b = Bot()
37
+ b._load_config(
38
+ Config(
39
+ username="grace",
40
+ token="abc123",
41
+ )
42
+ )
20
43
 
21
44
  b._client = MagicMock()
22
45
  b._client.room_send = AsyncMock()
@@ -50,7 +73,7 @@ def test_bot_init_with_config():
50
73
  bot = Bot()
51
74
  bot._load_config(Config(username="grace", password="grace1234"))
52
75
 
53
- assert bot.config.user_id == "grace"
76
+ assert bot.config.username == "grace"
54
77
  assert bot.config.password == "grace1234"
55
78
  assert bot.config.homeserver == "https://matrix.org"
56
79
 
@@ -411,17 +434,14 @@ async def start_and_stop(coro):
411
434
 
412
435
 
413
436
  @pytest.mark.asyncio
414
- async def test_run_uses_token():
415
- bot = Bot()
416
- bot._load_config("tests/config_fixture_token.yaml")
417
-
418
- bot._client.sync_forever = AsyncMock()
419
- bot._on_ready = AsyncMock()
437
+ async def test_run_uses_token(bot_with_token):
438
+ bot_with_token._client.sync_forever = AsyncMock()
439
+ bot_with_token._on_ready = AsyncMock()
420
440
 
421
441
  # unblock readiness
422
- bot._synced.set()
442
+ bot_with_token._synced.set()
423
443
 
424
- task = asyncio.create_task(bot.run())
444
+ task = asyncio.create_task(bot_with_token.run())
425
445
 
426
446
  await asyncio.sleep(0)
427
447
  await asyncio.sleep(0)
@@ -429,14 +449,22 @@ async def test_run_uses_token():
429
449
  task.cancel()
430
450
  await asyncio.gather(task, return_exceptions=True)
431
451
 
432
- assert bot._client.access_token == "abc123"
433
- bot._on_ready.assert_awaited_once()
434
- bot._client.sync_forever.assert_awaited_once()
452
+ assert bot_with_token._client.access_token == "abc123"
453
+ bot_with_token._on_ready.assert_awaited_once()
454
+ bot_with_token._client.sync_forever.assert_awaited_once()
435
455
 
436
456
 
437
457
  @pytest.mark.asyncio
438
458
  async def test_run_with_username_and_password(bot):
439
- bot._client.login = AsyncMock(return_value="login_resp")
459
+ assert bot.config.token is None
460
+
461
+ login_called = asyncio.Event()
462
+
463
+ async def mock_login(password):
464
+ login_called.set()
465
+ return "login_resp"
466
+
467
+ bot._client.login = AsyncMock(side_effect=mock_login)
440
468
  bot._client.sync_forever = AsyncMock()
441
469
  bot._on_ready = AsyncMock()
442
470
 
@@ -444,15 +472,13 @@ async def test_run_with_username_and_password(bot):
444
472
 
445
473
  task = asyncio.create_task(bot.run())
446
474
 
447
- await asyncio.sleep(0)
448
- await asyncio.sleep(0)
475
+ await asyncio.wait_for(login_called.wait(), timeout=1.0)
449
476
 
450
477
  task.cancel()
451
478
  await asyncio.gather(task, return_exceptions=True)
452
479
 
453
480
  bot._client.login.assert_awaited_once_with("grace1234")
454
481
  bot._on_ready.assert_awaited_once()
455
- bot._client.sync_forever.assert_awaited_once()
456
482
 
457
483
 
458
484
  def test_start_handles_keyboard_interrupt(caplog):
@@ -463,7 +489,12 @@ def test_start_handles_keyboard_interrupt(caplog):
463
489
 
464
490
  with patch.object(bot, "_load_config"):
465
491
  with caplog.at_level("INFO"):
466
- bot.start(config="tests/config_fixture.yaml")
492
+ bot.start(
493
+ config=Config(
494
+ username="grace",
495
+ password="grace1234",
496
+ )
497
+ )
467
498
 
468
499
  assert "bot interrupted by user" in caplog.text
469
500
  bot._client.close.assert_awaited_once()
@@ -0,0 +1,139 @@
1
+ import os
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+ import yaml
6
+
7
+ from matrix.config import Config
8
+ from matrix.errors import ConfigError
9
+
10
+
11
+ @pytest.fixture
12
+ def config_default():
13
+ return Config(username="grace", password="secret")
14
+
15
+
16
+ @pytest.fixture
17
+ def config_file(tmp_path):
18
+ config = tmp_path / "test.yaml"
19
+ config.write_text(
20
+ "USERNAME: '@bot:matrix.org'\n"
21
+ "PASSWORD: 'secret'\n"
22
+ "PREFIX: '!'\n"
23
+ "LOG_LEVEL: 'INFO'\n"
24
+ "bot:\n"
25
+ " main_channel: '!abc123:matrix.org'\n"
26
+ )
27
+ return config
28
+
29
+
30
+ @pytest.fixture
31
+ def config(config_file):
32
+ return Config(config_path=str(config_file))
33
+
34
+
35
+ def test_get__returns_top_level_value(config: Config) -> None:
36
+ assert config.get(key="LOG_LEVEL") == "INFO"
37
+
38
+
39
+ def test_get__returns_none_when_key_missing_and_no_default(config: Config) -> None:
40
+ assert config.get(key="MISSING") is None
41
+
42
+
43
+ def test_get__returns_default_when_key_missing(config: Config) -> None:
44
+ assert config.get(key="MISSING", default="fallback") == "fallback"
45
+
46
+
47
+ def test_get__returns_section_value(config: Config) -> None:
48
+ assert config.get(key="main_channel", section="bot") == "!abc123:matrix.org"
49
+
50
+
51
+ def test_get__returns_default_when_section_missing(config: Config) -> None:
52
+ assert (
53
+ config.get(key="main_channel", section="MISSING", default="fallback")
54
+ == "fallback"
55
+ )
56
+
57
+
58
+ def test_get__returns_default_when_section_key_missing(config: Config) -> None:
59
+ assert config.get(key="MISSING", section="bot", default="fallback") == "fallback"
60
+
61
+
62
+ def test_getitem__returns_value(config: Config) -> None:
63
+ assert config["LOG_LEVEL"] == "INFO"
64
+
65
+
66
+ def test_getitem__raises_key_error_when_missing(config: Config) -> None:
67
+ with pytest.raises(KeyError):
68
+ _ = config["MISSING"]
69
+
70
+
71
+ @pytest.mark.parametrize(
72
+ "attr,expected",
73
+ [
74
+ ("homeserver", "https://matrix.org"),
75
+ ("username", "grace"),
76
+ ("password", "secret"),
77
+ ("token", None),
78
+ ("prefix", "!"),
79
+ ],
80
+ )
81
+ def test_config_defaults_success(config_default, attr, expected):
82
+ assert getattr(config_default, attr) == expected
83
+
84
+
85
+ def test_loading_valid_yaml(tmp_path):
86
+ yaml_text = """
87
+ HOMESERVER: https://matrix.org
88
+ USERNAME: "@grace:matrix.org"
89
+ PASSWORD: grace1234
90
+ PREFIX: "/"
91
+ """
92
+ config_file = tmp_path / "good.yaml"
93
+ config_file.write_text(yaml_text)
94
+
95
+ cfg = Config(str(config_file))
96
+
97
+ assert cfg.username == "@grace:matrix.org"
98
+ assert cfg.password == "grace1234"
99
+ assert cfg.prefix == "/"
100
+
101
+
102
+ def test_file_not_found(tmp_path):
103
+ with pytest.raises(FileNotFoundError):
104
+ Config(str(tmp_path / "nope.yaml"))
105
+
106
+
107
+ def test_bad_yaml_syntax(tmp_path):
108
+ bad = tmp_path / "bad.yaml"
109
+ bad.write_text("not: valid: : yaml")
110
+ with pytest.raises(yaml.YAMLError):
111
+ Config(str(bad))
112
+
113
+
114
+ def test_missing_credentials_raises_ConfigError_kwargs():
115
+ with pytest.raises(ConfigError) as exc:
116
+ Config(username="only_user")
117
+ # the assert make sure that the error is raised from
118
+ # the constructor and not load_from_file method
119
+ assert "username and password or token" in str(exc.value)
120
+
121
+
122
+ def test_missing_credentials_raises_ConfigError_yaml(tmp_path):
123
+ yaml_text = "HOMESERVER: https://matrix.org\n" "PASSWORD: \n" "TOKEN: \n"
124
+ file = tmp_path / "err.yaml"
125
+ file.write_text(yaml_text)
126
+
127
+ with patch.dict(os.environ, {}, clear=True):
128
+ with pytest.raises(ConfigError) as exc:
129
+ Config(config_path=str(file))
130
+ assert "USERNAME and PASSWORD or TOKEN" in str(exc.value)
131
+
132
+
133
+ def test_token_only():
134
+ token = "my_very_secure_token"
135
+ cfg = Config(token=token)
136
+
137
+ assert cfg.token == token
138
+ assert cfg.password is None
139
+ assert cfg.homeserver == "https://matrix.org"
@@ -1,61 +0,0 @@
1
- import yaml
2
- from .errors import ConfigError
3
- from typing import Optional
4
-
5
-
6
- class Config:
7
- """
8
- Configuration handler for Matrix client settings. Including the following:
9
-
10
- homeserver: Defaults to 'https://matrix.org'
11
- user_id: The Matrix user ID (username).
12
- password: (Optional) One of the password or token must be provided.
13
- token: (Optional) One of the password or token must be provided.
14
- prefix: Defaults to '!' if not specified in the config file.
15
-
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.
22
-
23
- :raises FileNotFoundError: If the configuration file does not exist.
24
- :raises yaml.YAMLError: If the configuration file cannot be parsed.
25
- :raises ConfigError: If neither password or token has been provided.
26
- """
27
-
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 "!"
43
-
44
- if config_path:
45
- self.load_from_file(config_path)
46
- elif not (self.password or self.token):
47
- raise ConfigError("username and password or token")
48
-
49
- def load_from_file(self, config_path: str) -> None:
50
- """Load Matrix client settings via YAML config file."""
51
- with open(config_path, "r") as f:
52
- config = yaml.safe_load(f)
53
-
54
- if not (config.get("PASSWORD") or config.get("TOKEN")):
55
- raise ConfigError("USERNAME and PASSWORD or TOKEN")
56
-
57
- self.homeserver = config.get("HOMESERVER", "https://matrix.org")
58
- self.user_id = config.get("USERNAME")
59
- self.password = config.get("PASSWORD", None)
60
- self.token = config.get("TOKEN", None)
61
- self.prefix = config.get("PREFIX", "!")
@@ -1,13 +0,0 @@
1
- matrix-nio
2
- logger
3
- PyYAML
4
- markdown
5
- APScheduler
6
-
7
- [dev]
8
- pytest
9
- pytest-asyncio
10
- black
11
- mypy
12
- types-PyYAML
13
- types-Markdown
@@ -1,4 +0,0 @@
1
- HOMESERVER: "matrix.fixture.org"
2
- USERNAME: "grace"
3
- PASSWORD: "grace1234"
4
- PREFIX: "!"
@@ -1 +0,0 @@
1
- TOKEN: "abc123"
@@ -1,81 +0,0 @@
1
- import pytest
2
- import yaml
3
-
4
- from matrix.errors import ConfigError
5
- from matrix.config import Config
6
-
7
-
8
- @pytest.fixture
9
- def config_default():
10
- return Config(username="grace", password="secret")
11
-
12
-
13
- @pytest.mark.parametrize(
14
- "attr,expected",
15
- [
16
- ("homeserver", "https://matrix.org"),
17
- ("user_id", "grace"),
18
- ("password", "secret"),
19
- ("token", None),
20
- ("prefix", "!"),
21
- ],
22
- )
23
- def test_config_defaults_success(config_default, attr, expected):
24
- assert getattr(config_default, attr) == expected
25
-
26
-
27
- def test_loading_valid_yaml(tmp_path):
28
- yaml_text = """
29
- HOMESERVER: https://matrix.org
30
- USERNAME: "@grace:matrix.org"
31
- PASSWORD: grace1234
32
- PREFIX: "/"
33
- """
34
- config_file = tmp_path / "good.yaml"
35
- config_file.write_text(yaml_text)
36
-
37
- cfg = Config(str(config_file))
38
-
39
- assert cfg.user_id == "@grace:matrix.org"
40
- assert cfg.password == "grace1234"
41
- assert cfg.prefix == "/"
42
-
43
-
44
- def test_file_not_found(tmp_path):
45
- with pytest.raises(FileNotFoundError):
46
- Config(str(tmp_path / "nope.yaml"))
47
-
48
-
49
- def test_bad_yaml_syntax(tmp_path):
50
- bad = tmp_path / "bad.yaml"
51
- bad.write_text("not: valid: : yaml")
52
- with pytest.raises(yaml.YAMLError):
53
- Config(str(bad))
54
-
55
-
56
- def test_missing_credentials_raises_ConfigError_kwargs():
57
- with pytest.raises(ConfigError) as exc:
58
- Config(username="only_user")
59
- # the assert make sure that the error is raised from
60
- # the constructor and not load_from_file method
61
- assert "username and password or token" in str(exc.value)
62
-
63
-
64
- def test_missing_credentials_raises_ConfigError_yaml(tmp_path):
65
- yaml_text = "HOMESERVER: https://matrix.org"
66
- file = tmp_path / "err.yaml"
67
- file.write_text(yaml_text)
68
- with pytest.raises(ConfigError) as exc:
69
- Config(str(file))
70
- # the assert make sure that the error is raised from
71
- # the load_from_file method and not the constructor
72
- assert "USERNAME and PASSWORD or TOKEN" in str(exc.value)
73
-
74
-
75
- def test_token_only():
76
- token = "my_very_secure_token"
77
- cfg = Config(token=token)
78
-
79
- assert cfg.token == token
80
- assert cfg.password is None
81
- assert cfg.homeserver == "https://matrix.org"
File without changes