webex-message-handler 0.6.11__tar.gz → 0.6.12__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 (29) hide show
  1. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/PKG-INFO +3 -4
  2. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/pyproject.toml +3 -4
  3. webex_message_handler-0.6.12/tests/conftest.py +89 -0
  4. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/test_device_manager.py +67 -75
  5. webex_message_handler-0.6.11/tests/conftest.py +0 -40
  6. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/.gitignore +0 -0
  7. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/API.md +0 -0
  8. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/LICENSE +0 -0
  9. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/README.md +0 -0
  10. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/examples/basic_bot.py +0 -0
  11. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/__init__.py +0 -0
  12. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/device_manager.py +0 -0
  13. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/errors.py +0 -0
  14. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/handler.py +0 -0
  15. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/id_utils.py +0 -0
  16. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/kms_client.py +0 -0
  17. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/logger.py +0 -0
  18. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/mention_parser.py +0 -0
  19. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/mercury_socket.py +0 -0
  20. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/message_decryptor.py +0 -0
  21. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/types.py +0 -0
  22. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/src/webex_message_handler/url_validation.py +0 -0
  23. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/test-proxy.py +0 -0
  24. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/__init__.py +0 -0
  25. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/test_handler.py +0 -0
  26. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/test_id_utils.py +0 -0
  27. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/test_integration.py +0 -0
  28. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/test_kms_client.py +0 -0
  29. {webex_message_handler-0.6.11 → webex_message_handler-0.6.12}/tests/test_message_decryptor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webex-message-handler
3
- Version: 0.6.11
3
+ Version: 0.6.12
4
4
  Summary: Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK
5
5
  Project-URL: Homepage, https://github.com/3rg0n/webex-message-handler
6
6
  Project-URL: Repository, https://github.com/3rg0n/webex-message-handler
@@ -19,11 +19,10 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: Topic :: Communications :: Chat
21
21
  Requires-Python: >=3.10
22
- Requires-Dist: aiohttp>=3.13.4
23
- Requires-Dist: cryptography>=46.0.7
22
+ Requires-Dist: aiohttp>=3.14.1
23
+ Requires-Dist: cryptography>=48.0.1
24
24
  Requires-Dist: jwcrypto>=1.5.7
25
25
  Provides-Extra: dev
26
- Requires-Dist: aioresponses>=0.7; extra == 'dev'
27
26
  Requires-Dist: mypy>=1.10; extra == 'dev'
28
27
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
28
  Requires-Dist: pytest>=7; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "webex-message-handler"
7
- version = "0.6.11"
7
+ version = "0.6.12"
8
8
  description = "Lightweight Webex Mercury WebSocket + KMS decryption for receiving bot messages without the full Webex SDK"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -26,8 +26,8 @@ classifiers = [
26
26
  "Framework :: AsyncIO",
27
27
  ]
28
28
  dependencies = [
29
- "aiohttp>=3.13.4",
30
- "cryptography>=46.0.7",
29
+ "aiohttp>=3.14.1",
30
+ "cryptography>=48.0.1",
31
31
  "jwcrypto>=1.5.7",
32
32
  ]
33
33
 
@@ -57,7 +57,6 @@ strict = true
57
57
  dev = [
58
58
  "pytest>=7",
59
59
  "pytest-asyncio>=0.23",
60
- "aioresponses>=0.7",
61
60
  "ruff>=0.4",
62
61
  "mypy>=1.10",
63
62
  ]
@@ -0,0 +1,89 @@
1
+ """Pytest configuration and fixtures."""
2
+
3
+ import json as _json
4
+
5
+ import aiohttp
6
+
7
+ from webex_message_handler.types import FetchRequest, FetchResponse
8
+
9
+
10
+ class MockHttpResponse:
11
+ """A canned FetchResponse for the injected http_do adapter."""
12
+
13
+ def __init__(self, status: int = 200, payload=None, *, exc: Exception | None = None):
14
+ self.status = status
15
+ self.ok = 200 <= status < 300
16
+ self._payload = payload
17
+ self._exc = exc
18
+
19
+ async def json(self):
20
+ return self._payload
21
+
22
+ async def text(self):
23
+ return _json.dumps(self._payload) if self._payload is not None else ""
24
+
25
+
26
+ class MockHttpDo:
27
+ """Mocks HTTP at the injected ``http_do`` seam instead of patching aiohttp.
28
+
29
+ Register canned responses keyed by ``(METHOD, url)``; the adapter pops them
30
+ in registration order so repeated calls to the same endpoint can return
31
+ different responses. A registered ``Exception`` is raised to simulate a
32
+ network failure (mirroring the handler's try/except path).
33
+ """
34
+
35
+ def __init__(self):
36
+ self._routes: dict[tuple[str, str], list] = {}
37
+ self.calls: list[FetchRequest] = []
38
+
39
+ def add(self, method: str, url: str, *, status: int = 200, payload=None, exc: Exception | None = None):
40
+ self._routes.setdefault((method.upper(), url), []).append(
41
+ MockHttpResponse(status=status, payload=payload, exc=exc)
42
+ )
43
+ return self
44
+
45
+ async def __call__(self, request: FetchRequest) -> FetchResponse:
46
+ self.calls.append(request)
47
+ key = (request.method.upper(), request.url)
48
+ queue = self._routes.get(key)
49
+ if not queue:
50
+ raise AssertionError(f"unexpected request: {request.method} {request.url}")
51
+ response = queue.pop(0) if len(queue) > 1 else queue[0]
52
+ if response._exc is not None:
53
+ raise response._exc
54
+ return response # type: ignore[return-value]
55
+
56
+
57
+ async def create_test_http_do(connector: aiohttp.BaseConnector | None = None):
58
+ """Create a test HTTP adapter for use in tests."""
59
+ async def http_do(request: FetchRequest) -> FetchResponse:
60
+ async with (
61
+ aiohttp.ClientSession(connector=connector) as session,
62
+ session.request(
63
+ request.method,
64
+ request.url,
65
+ headers=request.headers,
66
+ data=request.body,
67
+ ) as response,
68
+ ):
69
+ # Eagerly read the response body before the context closes
70
+ body_bytes = await response.read()
71
+ status = response.status
72
+ ok = 200 <= status < 300
73
+
74
+ class TestFetchResponse:
75
+ def __init__(self):
76
+ self.status = status
77
+ self.ok = ok
78
+ self._body = body_bytes
79
+
80
+ async def json(self):
81
+ import json
82
+ return json.loads(self._body.decode('utf-8'))
83
+
84
+ async def text(self):
85
+ return self._body.decode('utf-8')
86
+
87
+ return TestFetchResponse() # type: ignore[return-value]
88
+
89
+ return http_do
@@ -1,9 +1,8 @@
1
1
  """Tests for DeviceManager."""
2
2
 
3
3
  import pytest
4
- from aioresponses import aioresponses
5
4
 
6
- from tests.conftest import create_test_http_do
5
+ from tests.conftest import MockHttpDo
7
6
  from webex_message_handler.device_manager import WDM_API_BASE, DeviceManager
8
7
  from webex_message_handler.errors import AuthError, DeviceRegistrationError
9
8
 
@@ -25,11 +24,9 @@ MOCK_WDM_RESPONSE = {
25
24
 
26
25
  class TestRegister:
27
26
  async def test_successful_registration(self):
28
- http_do = await create_test_http_do()
27
+ http_do = MockHttpDo().add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
29
28
  dm = DeviceManager(http_do=http_do)
30
- with aioresponses() as m:
31
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
32
- result = await dm.register(MOCK_TOKEN)
29
+ result = await dm.register(MOCK_TOKEN)
33
30
 
34
31
  assert result.web_socket_url == MOCK_WS_URL
35
32
  assert result.device_url == MOCK_DEVICE_URL
@@ -38,127 +35,122 @@ class TestRegister:
38
35
  assert result.services["messenger"] == "https://messenger.example.com"
39
36
 
40
37
  async def test_auth_error_on_401(self):
41
- http_do = await create_test_http_do()
38
+ http_do = MockHttpDo().add("POST", WDM_API_BASE, status=401)
42
39
  dm = DeviceManager(http_do=http_do)
43
- with aioresponses() as m:
44
- m.post(WDM_API_BASE, status=401)
45
- with pytest.raises(AuthError):
46
- await dm.register(MOCK_TOKEN)
40
+ with pytest.raises(AuthError):
41
+ await dm.register(MOCK_TOKEN)
47
42
 
48
43
  async def test_device_registration_error_on_non_2xx(self):
49
- http_do = await create_test_http_do()
44
+ http_do = MockHttpDo().add("POST", WDM_API_BASE, status=400)
50
45
  dm = DeviceManager(http_do=http_do)
51
- with aioresponses() as m:
52
- m.post(WDM_API_BASE, status=400)
53
- with pytest.raises(DeviceRegistrationError, match="Failed to register device"):
54
- await dm.register(MOCK_TOKEN)
46
+ with pytest.raises(DeviceRegistrationError, match="Failed to register device"):
47
+ await dm.register(MOCK_TOKEN)
55
48
 
56
49
  async def test_device_registration_error_on_network_failure(self):
57
- http_do = await create_test_http_do()
50
+ http_do = MockHttpDo().add("POST", WDM_API_BASE, exc=ConnectionError("Network error"))
58
51
  dm = DeviceManager(http_do=http_do)
59
- with aioresponses() as m:
60
- m.post(WDM_API_BASE, exception=ConnectionError("Network error"))
61
- with pytest.raises(DeviceRegistrationError):
62
- await dm.register(MOCK_TOKEN)
52
+ with pytest.raises(DeviceRegistrationError):
53
+ await dm.register(MOCK_TOKEN)
63
54
 
64
55
 
65
56
  class TestRefresh:
66
57
  async def test_successful_refresh(self):
67
- http_do = await create_test_http_do()
58
+ refreshed = {**MOCK_WDM_RESPONSE, "webSocketUrl": "wss://mercury-new.example.com/socket"}
59
+ http_do = (
60
+ MockHttpDo()
61
+ .add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
62
+ .add("PUT", MOCK_DEVICE_URL, payload=refreshed)
63
+ )
68
64
  dm = DeviceManager(http_do=http_do)
69
- with aioresponses() as m:
70
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
71
- await dm.register(MOCK_TOKEN)
72
-
73
- refreshed = {**MOCK_WDM_RESPONSE, "webSocketUrl": "wss://mercury-new.example.com/socket"}
74
- m.put(MOCK_DEVICE_URL, payload=refreshed)
75
- result = await dm.refresh(MOCK_TOKEN)
65
+ await dm.register(MOCK_TOKEN)
66
+ result = await dm.refresh(MOCK_TOKEN)
76
67
 
77
68
  assert result.web_socket_url == "wss://mercury-new.example.com/socket"
78
69
 
79
70
  async def test_error_if_not_registered(self):
80
- http_do = await create_test_http_do()
71
+ http_do = MockHttpDo()
81
72
  dm = DeviceManager(http_do=http_do)
82
73
  with pytest.raises(DeviceRegistrationError, match="Device not registered"):
83
74
  await dm.refresh(MOCK_TOKEN)
84
75
 
85
76
  async def test_auth_error_on_401_during_refresh(self):
86
- http_do = await create_test_http_do()
77
+ http_do = (
78
+ MockHttpDo()
79
+ .add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
80
+ .add("PUT", MOCK_DEVICE_URL, status=401)
81
+ )
87
82
  dm = DeviceManager(http_do=http_do)
88
- with aioresponses() as m:
89
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
90
- await dm.register(MOCK_TOKEN)
91
- m.put(MOCK_DEVICE_URL, status=401)
92
- with pytest.raises(AuthError):
93
- await dm.refresh(MOCK_TOKEN)
83
+ await dm.register(MOCK_TOKEN)
84
+ with pytest.raises(AuthError):
85
+ await dm.refresh(MOCK_TOKEN)
94
86
 
95
87
  async def test_device_registration_error_on_refresh_failure(self):
96
- http_do = await create_test_http_do()
88
+ http_do = (
89
+ MockHttpDo()
90
+ .add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
91
+ .add("PUT", MOCK_DEVICE_URL, status=500)
92
+ )
97
93
  dm = DeviceManager(http_do=http_do)
98
- with aioresponses() as m:
99
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
100
- await dm.register(MOCK_TOKEN)
101
- m.put(MOCK_DEVICE_URL, status=500)
102
- with pytest.raises(DeviceRegistrationError):
103
- await dm.refresh(MOCK_TOKEN)
94
+ await dm.register(MOCK_TOKEN)
95
+ with pytest.raises(DeviceRegistrationError):
96
+ await dm.refresh(MOCK_TOKEN)
104
97
 
105
98
 
106
99
  class TestUnregister:
107
100
  async def test_successful_unregister(self):
108
- http_do = await create_test_http_do()
101
+ http_do = (
102
+ MockHttpDo()
103
+ .add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
104
+ .add("DELETE", MOCK_DEVICE_URL, status=204)
105
+ )
109
106
  dm = DeviceManager(http_do=http_do)
110
- with aioresponses() as m:
111
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
112
- await dm.register(MOCK_TOKEN)
113
- m.delete(MOCK_DEVICE_URL, status=204)
114
- await dm.unregister(MOCK_TOKEN)
107
+ await dm.register(MOCK_TOKEN)
108
+ await dm.unregister(MOCK_TOKEN)
115
109
 
116
110
  async def test_error_if_not_registered(self):
117
- http_do = await create_test_http_do()
111
+ http_do = MockHttpDo()
118
112
  dm = DeviceManager(http_do=http_do)
119
113
  with pytest.raises(DeviceRegistrationError, match="Device not registered"):
120
114
  await dm.unregister(MOCK_TOKEN)
121
115
 
122
116
  async def test_auth_error_on_401_during_unregister(self):
123
- http_do = await create_test_http_do()
117
+ http_do = (
118
+ MockHttpDo()
119
+ .add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
120
+ .add("DELETE", MOCK_DEVICE_URL, status=401)
121
+ )
124
122
  dm = DeviceManager(http_do=http_do)
125
- with aioresponses() as m:
126
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
127
- await dm.register(MOCK_TOKEN)
128
- m.delete(MOCK_DEVICE_URL, status=401)
129
- with pytest.raises(AuthError):
130
- await dm.unregister(MOCK_TOKEN)
123
+ await dm.register(MOCK_TOKEN)
124
+ with pytest.raises(AuthError):
125
+ await dm.unregister(MOCK_TOKEN)
131
126
 
132
127
  async def test_device_registration_error_on_unregister_failure(self):
133
- http_do = await create_test_http_do()
128
+ http_do = (
129
+ MockHttpDo()
130
+ .add("POST", WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
131
+ .add("DELETE", MOCK_DEVICE_URL, status=500)
132
+ )
134
133
  dm = DeviceManager(http_do=http_do)
135
- with aioresponses() as m:
136
- m.post(WDM_API_BASE, payload=MOCK_WDM_RESPONSE)
137
- await dm.register(MOCK_TOKEN)
138
- m.delete(MOCK_DEVICE_URL, status=500)
139
- with pytest.raises(DeviceRegistrationError):
140
- await dm.unregister(MOCK_TOKEN)
134
+ await dm.register(MOCK_TOKEN)
135
+ with pytest.raises(DeviceRegistrationError):
136
+ await dm.unregister(MOCK_TOKEN)
141
137
 
142
138
 
143
139
  class TestServiceParsing:
144
140
  async def test_empty_services(self):
145
- http_do = await create_test_http_do()
146
- dm = DeviceManager(http_do=http_do)
147
141
  response = {**MOCK_WDM_RESPONSE, "services": {}}
148
- with aioresponses() as m:
149
- m.post(WDM_API_BASE, payload=response)
150
- result = await dm.register(MOCK_TOKEN)
142
+ http_do = MockHttpDo().add("POST", WDM_API_BASE, payload=response)
143
+ dm = DeviceManager(http_do=http_do)
144
+ result = await dm.register(MOCK_TOKEN)
151
145
 
152
146
  assert result.services == {}
153
147
  assert result.encryption_service_url == ""
154
148
 
155
149
  async def test_missing_encryption_service_url(self):
156
- http_do = await create_test_http_do()
157
- dm = DeviceManager(http_do=http_do)
158
150
  response = {**MOCK_WDM_RESPONSE, "services": {"messenger": "https://messenger.example.com"}}
159
- with aioresponses() as m:
160
- m.post(WDM_API_BASE, payload=response)
161
- result = await dm.register(MOCK_TOKEN)
151
+ http_do = MockHttpDo().add("POST", WDM_API_BASE, payload=response)
152
+ dm = DeviceManager(http_do=http_do)
153
+ result = await dm.register(MOCK_TOKEN)
162
154
 
163
155
  assert "messenger" in result.services
164
156
  assert result.encryption_service_url == ""
@@ -1,40 +0,0 @@
1
- """Pytest configuration and fixtures."""
2
-
3
- import aiohttp
4
-
5
- from webex_message_handler.types import FetchRequest, FetchResponse
6
-
7
-
8
- async def create_test_http_do(connector: aiohttp.BaseConnector | None = None):
9
- """Create a test HTTP adapter for use in tests."""
10
- async def http_do(request: FetchRequest) -> FetchResponse:
11
- async with (
12
- aiohttp.ClientSession(connector=connector) as session,
13
- session.request(
14
- request.method,
15
- request.url,
16
- headers=request.headers,
17
- data=request.body,
18
- ) as response,
19
- ):
20
- # Eagerly read the response body before the context closes
21
- body_bytes = await response.read()
22
- status = response.status
23
- ok = 200 <= status < 300
24
-
25
- class TestFetchResponse:
26
- def __init__(self):
27
- self.status = status
28
- self.ok = ok
29
- self._body = body_bytes
30
-
31
- async def json(self):
32
- import json
33
- return json.loads(self._body.decode('utf-8'))
34
-
35
- async def text(self):
36
- return self._body.decode('utf-8')
37
-
38
- return TestFetchResponse() # type: ignore[return-value]
39
-
40
- return http_do