matrix-python 1.4.5a0__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.
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/PKG-INFO +2 -1
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/_version.py +3 -3
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/bot.py +2 -3
- matrix_python-1.4.6a0/matrix/config.py +125 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/PKG-INFO +2 -1
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/SOURCES.txt +0 -2
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/requires.txt +1 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/pyproject.toml +1 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_bot.py +49 -18
- matrix_python-1.4.6a0/tests/test_config.py +139 -0
- matrix_python-1.4.5a0/matrix/config.py +0 -61
- matrix_python-1.4.5a0/tests/config_fixture.yaml +0 -4
- matrix_python-1.4.5a0/tests/config_fixture_token.yaml +0 -1
- matrix_python-1.4.5a0/tests/test_config.py +0 -81
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.github/dependabot.yml +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.github/workflows/CODEOWNERS +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.github/workflows/codeql.yml +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.github/workflows/publish.yml +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.github/workflows/scorecard.yml +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.github/workflows/tests.yml +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/.gitignore +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/CODE_OF_CONDUCT.md +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/CONTRIBUTING.md +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/LICENSE +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/README.md +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/README.md +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/checks.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/config.yaml +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/cooldown.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/error_handling.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/extension.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/ping.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/reaction.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/examples/scheduler.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/__init__.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/checks.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/command.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/content.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/context.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/errors.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/extension.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/group.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/help/__init__.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/help/help_command.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/help/pagination.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/message.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/protocols.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/registry.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/room.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/scheduler.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix/types.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/dependency_links.txt +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/matrix_python.egg-info/top_level.txt +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/mypy.ini +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/setup.cfg +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/help/test_default_help_command.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/help/test_help_command.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/help/test_pagination.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_command.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_context.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_extension.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_group.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_message.py +0 -0
- {matrix_python-1.4.5a0 → matrix_python-1.4.6a0}/tests/test_registry.py +0 -0
- {matrix_python-1.4.5a0 → 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.
|
|
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>
|
|
@@ -689,6 +689,7 @@ Requires-Dist: logger
|
|
|
689
689
|
Requires-Dist: PyYAML==6.0.3
|
|
690
690
|
Requires-Dist: markdown==3.10.2
|
|
691
691
|
Requires-Dist: APScheduler==3.11.2
|
|
692
|
+
Requires-Dist: envyaml==1.10.211231
|
|
692
693
|
Provides-Extra: dev
|
|
693
694
|
Requires-Dist: pytest==9.0.3; extra == "dev"
|
|
694
695
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 4,
|
|
21
|
+
__version__ = version = '1.4.6a0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 6, 'a0')
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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>
|
|
@@ -689,6 +689,7 @@ Requires-Dist: logger
|
|
|
689
689
|
Requires-Dist: PyYAML==6.0.3
|
|
690
690
|
Requires-Dist: markdown==3.10.2
|
|
691
691
|
Requires-Dist: APScheduler==3.11.2
|
|
692
|
+
Requires-Dist: envyaml==1.10.211231
|
|
692
693
|
Provides-Extra: dev
|
|
693
694
|
Requires-Dist: pytest==9.0.3; extra == "dev"
|
|
694
695
|
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
|
|
@@ -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
|
|
@@ -16,7 +16,30 @@ from matrix.errors import (
|
|
|
16
16
|
@pytest.fixture
|
|
17
17
|
def bot():
|
|
18
18
|
b = Bot()
|
|
19
|
-
b._load_config(
|
|
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.
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
442
|
+
bot_with_token._synced.set()
|
|
423
443
|
|
|
424
|
-
task = asyncio.create_task(
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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 +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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|