webex-bot 1.0.8__tar.gz → 1.1.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 (44) hide show
  1. {webex_bot-1.0.8 → webex_bot-1.1.12}/CONTRIBUTING.rst +8 -7
  2. {webex_bot-1.0.8 → webex_bot-1.1.12}/MANIFEST.in +2 -0
  3. {webex_bot-1.0.8/webex_bot.egg-info → webex_bot-1.1.12}/PKG-INFO +17 -22
  4. {webex_bot-1.0.8 → webex_bot-1.1.12}/README.md +12 -4
  5. {webex_bot-1.0.8 → webex_bot-1.1.12}/setup.cfg +2 -1
  6. {webex_bot-1.0.8 → webex_bot-1.1.12}/setup.py +3 -3
  7. webex_bot-1.1.12/tests/conftest.py +116 -0
  8. webex_bot-1.1.12/tests/test_command_model.py +49 -0
  9. webex_bot-1.1.12/tests/test_commands.py +49 -0
  10. webex_bot-1.1.12/tests/test_exceptions.py +8 -0
  11. webex_bot-1.1.12/tests/test_formatting.py +12 -0
  12. webex_bot-1.1.12/tests/test_response.py +32 -0
  13. webex_bot-1.1.12/tests/test_webex_bot.py +149 -0
  14. webex_bot-1.1.12/tests/test_webex_websocket_client.py +37 -0
  15. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/__init__.py +1 -1
  16. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/commands/echo.py +1 -0
  17. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/webex_bot.py +52 -37
  18. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/websockets/webex_websocket_client.py +41 -8
  19. {webex_bot-1.0.8 → webex_bot-1.1.12/webex_bot.egg-info}/PKG-INFO +17 -22
  20. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/SOURCES.txt +7 -0
  21. webex_bot-1.1.12/webex_bot.egg-info/requires.txt +7 -0
  22. webex_bot-1.0.8/tests/test_webex_bot.py +0 -21
  23. webex_bot-1.0.8/webex_bot.egg-info/requires.txt +0 -7
  24. {webex_bot-1.0.8 → webex_bot-1.1.12}/LICENSE +0 -0
  25. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/Makefile +0 -0
  26. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/conf.py +0 -0
  27. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/contributing.rst +0 -0
  28. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/index.rst +0 -0
  29. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/installation.rst +0 -0
  30. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/make.bat +0 -0
  31. {webex_bot-1.0.8 → webex_bot-1.1.12}/docs/usage.rst +0 -0
  32. {webex_bot-1.0.8 → webex_bot-1.1.12}/tests/__init__.py +0 -0
  33. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/cards/__init__.py +0 -0
  34. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/commands/__init__.py +0 -0
  35. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/commands/help.py +0 -0
  36. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/exceptions.py +0 -0
  37. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/formatting.py +0 -0
  38. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/models/__init__.py +0 -0
  39. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/models/command.py +0 -0
  40. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/models/response.py +0 -0
  41. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot/websockets/__init__.py +0 -0
  42. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/dependency_links.txt +0 -0
  43. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/not-zip-safe +0 -0
  44. {webex_bot-1.0.8 → webex_bot-1.1.12}/webex_bot.egg-info/top_level.txt +0 -0
@@ -80,10 +80,12 @@ Ready to contribute? Here's how to set up `webex_bot` for local development.
80
80
  tests, including testing other Python versions with tox::
81
81
 
82
82
  $ flake8 webex_bot tests
83
- $ python setup.py test or pytest
83
+ $ pytest
84
84
  $ tox
85
85
 
86
- To get flake8 and tox, just pip install them into your virtualenv.
86
+ To get the dev dependencies, install them into your virtualenv::
87
+
88
+ $ python -m pip install -r requirements_dev.txt
87
89
 
88
90
  6. Commit your changes and push your branch to GitHub::
89
91
 
@@ -102,9 +104,8 @@ Before you submit a pull request, check that it meets these guidelines:
102
104
  2. If the pull request adds functionality, the docs should be updated. Put
103
105
  your new functionality into a function with a docstring, and add the
104
106
  feature to the list in README.rst.
105
- 3. The pull request should work for Python 3.10, 3.11, 3.12, and 3.13, and for PyPy. Check
106
- https://travis-ci.com/fbradyirl/webex_bot/pull_requests
107
- and make sure that the tests pass for all supported Python versions.
107
+ 3. The pull request should work for Python 3.10, 3.11, and 3.12. Check GitHub
108
+ Actions and make sure that the tests pass for all supported Python versions.
108
109
 
109
110
  Tips
110
111
  ----
@@ -121,8 +122,8 @@ A reminder for the maintainers on how to deploy.
121
122
  Make sure all your changes are committed
122
123
  Then run::
123
124
 
124
- $ bump2version patch # possible: major / minor / patch
125
+ $ bump2version patch # possible: major / minor / patch (updates webex_bot/__init__.py)
125
126
  $ git push
126
127
  $ git push --tags
127
128
 
128
- Travis will then deploy to PyPI if tests pass.
129
+ GitHub Actions will then deploy to PyPI if tests pass.
@@ -1,6 +1,8 @@
1
1
  include CONTRIBUTING.rst
2
2
  include LICENSE
3
3
  include README.md
4
+ include pyproject.toml
5
+ include setup.cfg
4
6
 
5
7
  recursive-include tests *
6
8
  recursive-exclude * __pycache__
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: webex_bot
3
- Version: 1.0.8
3
+ Version: 1.1.12
4
4
  Summary: Python package for a Webex Bot based on websockets.
5
5
  Home-page: https://github.com/fbradyirl/webex_bot
6
6
  Author: Finbarr Brady
@@ -18,29 +18,16 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: webexpythonsdk==2.0.4
21
+ Requires-Dist: webexpythonsdk==2.0.5
22
22
  Requires-Dist: coloredlogs
23
- Requires-Dist: websockets==11.0.3
23
+ Requires-Dist: websockets==16
24
24
  Requires-Dist: backoff
25
25
  Provides-Extra: proxy
26
- Requires-Dist: websockets_proxy>=0.1.1; extra == "proxy"
27
- Dynamic: author
28
- Dynamic: author-email
29
- Dynamic: classifier
30
- Dynamic: description
31
- Dynamic: description-content-type
32
- Dynamic: home-page
33
- Dynamic: keywords
34
- Dynamic: license
35
- Dynamic: license-file
36
- Dynamic: provides-extra
37
- Dynamic: requires-dist
38
- Dynamic: requires-python
39
- Dynamic: summary
26
+ Requires-Dist: websockets_proxy>=0.1.3; extra == "proxy"
40
27
 
41
28
  # Introduction
42
29
 
43
- [![Pypi](https://img.shields.io/pypi/v/webex_bot.svg)](https://pypi.python.org/pypi/webex_bot) [![Build Status](https://github.com/fbradyirl/webex_bot/workflows/Python%20package/badge.svg)](https://github.com/fbradyirl/webex_bot/actions)
30
+ [![Pypi](https://img.shields.io/pypi/v/webex_bot.svg)](https://pypi.python.org/pypi/webex_bot) [![Build Status](https://github.com/fbradyirl/webex_bot/workflows/Python%20package/badge.svg)](https://github.com/fbradyirl/webex_bot/actions) [![CodeQL](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml/badge.svg)](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml) [![Coverage](https://codecov.io/gh/fbradyirl/webex_bot/branch/main/graph/badge.svg)](https://codecov.io/gh/fbradyirl/webex_bot) [![Release](https://img.shields.io/github/v/release/fbradyirl/webex_bot?sort=semver)](https://github.com/fbradyirl/webex_bot/releases) [![Downloads](https://img.shields.io/pypi/dm/webex_bot)](https://pypi.python.org/pypi/webex_bot)
44
31
 
45
32
  > [!IMPORTANT]
46
33
  > This repository is only sporadically maintained. Breaking API changes will be maintained on a best efforts basis.
@@ -50,7 +37,7 @@ Dynamic: summary
50
37
  > Bug reports unrelated to API changes may not get the attention you want.
51
38
 
52
39
 
53
- By using this module, you can create a [Webex Teams][5] messaging bot quickly in just a couple of lines of code.
40
+ By using this module, you can create a [Webex][5] messaging bot quickly in just a couple of lines of code.
54
41
 
55
42
  This module does not require you to set up an ngrok tunnel to receive incoming messages when behind a firewall or
56
43
  inside a LAN. This package instead uses a websocket to receive messages from the Webex cloud.
@@ -75,7 +62,7 @@ You can find a sample project, using OpenAI/ChatGPT with this library here: http
75
62
 
76
63
  ----
77
64
 
78
- **Only Python 3.13 is tested at this time.**
65
+ **Python 3.10, 3.11, and 3.12 are tested at this time.**
79
66
 
80
67
  1. Install this module from pypi:
81
68
 
@@ -113,7 +100,7 @@ proxies = {
113
100
  # Create a Bot Object
114
101
  bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN"),
115
102
  approved_rooms=['06586d8d-6aad-4201-9a69-0bf9eeb5766e'],
116
- bot_name="My Teams Ops Bot",
103
+ bot_name="My Webex Ops Bot",
117
104
  include_demo_commands=True,
118
105
  proxies=proxies)
119
106
 
@@ -464,6 +451,14 @@ bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN")
464
451
 
465
452
  * Allow flag to disable bot to bot check
466
453
 
454
+ ### 1.1.6 (2026-Feb-02)
455
+
456
+ * Update dependancies.
457
+ * Expand test coverage and add CI matrix for Python 3.10–3.12
458
+ * Add coverage reporting and update contributor docs
459
+ * Refresh websocket status handling to avoid deprecations
460
+
461
+
467
462
  [1]: https://github.com/aaugustin/websockets
468
463
 
469
464
  [2]: https://github.com/WebexCommunity/WebexPythonSDK
@@ -1,6 +1,6 @@
1
1
  # Introduction
2
2
 
3
- [![Pypi](https://img.shields.io/pypi/v/webex_bot.svg)](https://pypi.python.org/pypi/webex_bot) [![Build Status](https://github.com/fbradyirl/webex_bot/workflows/Python%20package/badge.svg)](https://github.com/fbradyirl/webex_bot/actions)
3
+ [![Pypi](https://img.shields.io/pypi/v/webex_bot.svg)](https://pypi.python.org/pypi/webex_bot) [![Build Status](https://github.com/fbradyirl/webex_bot/workflows/Python%20package/badge.svg)](https://github.com/fbradyirl/webex_bot/actions) [![CodeQL](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml/badge.svg)](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml) [![Coverage](https://codecov.io/gh/fbradyirl/webex_bot/branch/main/graph/badge.svg)](https://codecov.io/gh/fbradyirl/webex_bot) [![Release](https://img.shields.io/github/v/release/fbradyirl/webex_bot?sort=semver)](https://github.com/fbradyirl/webex_bot/releases) [![Downloads](https://img.shields.io/pypi/dm/webex_bot)](https://pypi.python.org/pypi/webex_bot)
4
4
 
5
5
  > [!IMPORTANT]
6
6
  > This repository is only sporadically maintained. Breaking API changes will be maintained on a best efforts basis.
@@ -10,7 +10,7 @@
10
10
  > Bug reports unrelated to API changes may not get the attention you want.
11
11
 
12
12
 
13
- By using this module, you can create a [Webex Teams][5] messaging bot quickly in just a couple of lines of code.
13
+ By using this module, you can create a [Webex][5] messaging bot quickly in just a couple of lines of code.
14
14
 
15
15
  This module does not require you to set up an ngrok tunnel to receive incoming messages when behind a firewall or
16
16
  inside a LAN. This package instead uses a websocket to receive messages from the Webex cloud.
@@ -35,7 +35,7 @@ You can find a sample project, using OpenAI/ChatGPT with this library here: http
35
35
 
36
36
  ----
37
37
 
38
- **Only Python 3.13 is tested at this time.**
38
+ **Python 3.10, 3.11, and 3.12 are tested at this time.**
39
39
 
40
40
  1. Install this module from pypi:
41
41
 
@@ -73,7 +73,7 @@ proxies = {
73
73
  # Create a Bot Object
74
74
  bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN"),
75
75
  approved_rooms=['06586d8d-6aad-4201-9a69-0bf9eeb5766e'],
76
- bot_name="My Teams Ops Bot",
76
+ bot_name="My Webex Ops Bot",
77
77
  include_demo_commands=True,
78
78
  proxies=proxies)
79
79
 
@@ -424,6 +424,14 @@ bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN")
424
424
 
425
425
  * Allow flag to disable bot to bot check
426
426
 
427
+ ### 1.1.6 (2026-Feb-02)
428
+
429
+ * Update dependancies.
430
+ * Expand test coverage and add CI matrix for Python 3.10–3.12
431
+ * Add coverage reporting and update contributor docs
432
+ * Refresh websocket status handling to avoid deprecations
433
+
434
+
427
435
  [1]: https://github.com/aaugustin/websockets
428
436
 
429
437
  [2]: https://github.com/WebexCommunity/WebexPythonSDK
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 1.0.8
2
+ current_version = 1.1.12
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -22,6 +22,7 @@ test = pytest
22
22
 
23
23
  [tool:pytest]
24
24
  collect_ignore = ['setup.py']
25
+ addopts = --cov=webex_bot --cov-report=term-missing --cov-report=xml --cov-report=html --cov-fail-under=80
25
26
 
26
27
  [egg_info]
27
28
  tag_build =
@@ -7,14 +7,14 @@ from setuptools import setup, find_packages
7
7
  with open('README.md') as readme_file:
8
8
  readme = readme_file.read()
9
9
 
10
- requirements = ['webexpythonsdk==2.0.4', 'coloredlogs', 'websockets==11.0.3', 'backoff']
10
+ requirements = ['webexpythonsdk==2.0.5', 'coloredlogs', 'websockets==16', 'backoff']
11
11
 
12
12
  setup_requirements = ['pytest-runner', ]
13
13
 
14
14
  test_requirements = ['pytest>=3', ]
15
15
 
16
16
  extras_requirements = {
17
- "proxy": ["websockets_proxy>=0.1.1"]
17
+ "proxy": ["websockets_proxy>=0.1.3"]
18
18
  }
19
19
 
20
20
  setup(
@@ -45,6 +45,6 @@ setup(
45
45
  test_suite='tests',
46
46
  tests_require=test_requirements,
47
47
  url='https://github.com/fbradyirl/webex_bot',
48
- version='1.0.8',
48
+ version='1.1.12',
49
49
  zip_safe=False,
50
50
  )
@@ -0,0 +1,116 @@
1
+ import types
2
+ import warnings
3
+
4
+ import pytest
5
+
6
+ warnings.filterwarnings(
7
+ "ignore",
8
+ category=PendingDeprecationWarning,
9
+ module=r"webexpythonsdk\.environment",
10
+ )
11
+ warnings.filterwarnings(
12
+ "ignore",
13
+ category=DeprecationWarning,
14
+ module=r"websockets\..*",
15
+ )
16
+
17
+ from webex_bot.webex_bot import WebexBot
18
+ from webex_bot.websockets.webex_websocket_client import WebexWebsocketClient
19
+
20
+
21
+ class DummyPerson:
22
+ def __init__(self, display_name="Webex Bot", email="bot@example.com", avatar="https://example.com/avatar.png"):
23
+ self.displayName = display_name
24
+ self.emails = [email]
25
+ self.avatar = avatar
26
+ self.type = "bot"
27
+
28
+
29
+ class DummyPeople:
30
+ def __init__(self, me_person):
31
+ self._me_person = me_person
32
+
33
+ def me(self):
34
+ return self._me_person
35
+
36
+
37
+ class DummyMessages:
38
+ def __init__(self):
39
+ self.created = []
40
+ self.deleted = []
41
+
42
+ def create(self, **kwargs):
43
+ self.created.append(kwargs)
44
+ return types.SimpleNamespace(id="message-1")
45
+
46
+ def delete(self, message_id):
47
+ self.deleted.append(message_id)
48
+
49
+
50
+ class DummyMemberships:
51
+ def __init__(self, member_emails=None):
52
+ self.member_emails = set(member_emails or [])
53
+
54
+ def list(self, roomId, personEmail):
55
+ if personEmail in self.member_emails:
56
+ return [types.SimpleNamespace(personEmail=personEmail)]
57
+ return []
58
+
59
+
60
+ class DummyTeams:
61
+ def __init__(self, person_email="bot@example.com", member_emails=None):
62
+ self.people = DummyPeople(DummyPerson(email=person_email))
63
+ self.messages = DummyMessages()
64
+ self.memberships = DummyMemberships(member_emails=member_emails)
65
+
66
+
67
+ @pytest.fixture
68
+ def teams_api():
69
+ return DummyTeams(member_emails={"member@example.com"})
70
+
71
+
72
+ @pytest.fixture
73
+ def bot(monkeypatch, teams_api):
74
+ def fake_init(self, access_token, bot_name, on_message=None, on_card_action=None, proxies=None):
75
+ self.access_token = access_token
76
+ self.teams = teams_api
77
+ self.on_message = on_message
78
+ self.on_card_action = on_card_action
79
+ self.proxies = proxies
80
+ self.websocket = None
81
+
82
+ monkeypatch.setattr(WebexWebsocketClient, "__init__", fake_init)
83
+ return WebexBot(teams_bot_token="test-token", include_demo_commands=False)
84
+
85
+
86
+ def make_activity(actor_type="PERSON", actor_email="user@example.com", tags=None, parent=None, activity_id="act-1"):
87
+ if tags is None:
88
+ tags = ["ONE_ON_ONE"]
89
+ activity = {
90
+ "id": activity_id,
91
+ "actor": {"type": actor_type, "emailAddress": actor_email},
92
+ "target": {"tags": tags},
93
+ }
94
+ if parent is not None:
95
+ activity["parent"] = parent
96
+ return activity
97
+
98
+
99
+ @pytest.fixture
100
+ def one_on_one_activity():
101
+ return make_activity()
102
+
103
+
104
+ @pytest.fixture
105
+ def group_activity():
106
+ return make_activity(tags=["GROUP"])
107
+
108
+
109
+ @pytest.fixture
110
+ def teams_message():
111
+ return types.SimpleNamespace(
112
+ roomId="room-1",
113
+ text="help",
114
+ personEmail="user@example.com",
115
+ messageId="msg-1",
116
+ )
@@ -0,0 +1,49 @@
1
+ from webex_bot.models.command import Command, CALLBACK_KEYWORD_KEY, COMMAND_KEYWORD_KEY
2
+
3
+
4
+ class SimpleCommand(Command):
5
+ def __init__(self, command_keyword=None, card=None):
6
+ super().__init__(command_keyword=command_keyword, card=card)
7
+
8
+ def execute(self, message, attachment_actions, activity):
9
+ return "ok"
10
+
11
+
12
+ def test_command_uses_callback_keyword_from_card():
13
+ card = {
14
+ "actions": [
15
+ {
16
+ "type": "Action.Submit",
17
+ "data": {CALLBACK_KEYWORD_KEY: "cb"},
18
+ }
19
+ ]
20
+ }
21
+ command = SimpleCommand(command_keyword="ping", card=card)
22
+ assert command.card_callback_keyword == "cb"
23
+
24
+
25
+ def test_command_uses_command_keyword_from_card():
26
+ card = {
27
+ "actions": [
28
+ {
29
+ "type": "Action.Submit",
30
+ "data": {COMMAND_KEYWORD_KEY: "cmd"},
31
+ }
32
+ ]
33
+ }
34
+ command = SimpleCommand(command_keyword=None, card=card)
35
+ assert command.command_keyword == "cmd"
36
+
37
+
38
+ def test_command_sets_default_callback_keyword_when_missing():
39
+ card = {
40
+ "actions": [
41
+ {
42
+ "type": "Action.Submit",
43
+ "data": {},
44
+ }
45
+ ]
46
+ }
47
+ command = SimpleCommand(command_keyword="ping", card=card)
48
+ assert command.card_callback_keyword == "callback___ping"
49
+ assert card["actions"][0]["data"][CALLBACK_KEYWORD_KEY] == "callback___ping"
@@ -0,0 +1,49 @@
1
+ import types
2
+
3
+ from webex_bot.commands.echo import EchoCommand, EchoCallback
4
+ from webex_bot.commands.help import HelpCommand, HELP_COMMAND_KEYWORD
5
+ from webex_bot.models.command import Command, COMMAND_KEYWORD_KEY
6
+ from webex_bot.models.response import Response
7
+
8
+
9
+ class SimpleCommand(Command):
10
+ def __init__(self, keyword, help_message="Help me"):
11
+ super().__init__(command_keyword=keyword, help_message=help_message)
12
+
13
+ def execute(self, message, attachment_actions, activity):
14
+ return "ok"
15
+
16
+
17
+ def test_echo_command_returns_response():
18
+ command = EchoCommand()
19
+ response = command.execute("ignored", None, {})
20
+ assert isinstance(response, Response)
21
+ assert response.attachments[0]["contentType"] == "application/vnd.microsoft.card.adaptive"
22
+
23
+
24
+ def test_echo_callback_uses_input_text():
25
+ command = EchoCallback()
26
+ attachment_actions = types.SimpleNamespace(inputs={"message_typed": "hello"})
27
+ result = command.execute("", attachment_actions, {})
28
+ assert "hello" in result
29
+
30
+
31
+ def test_echo_command_pre_execute_returns_response():
32
+ command = EchoCommand()
33
+ response = command.pre_execute("", types.SimpleNamespace(inputs={}), {})
34
+ assert isinstance(response, Response)
35
+
36
+
37
+ def test_help_command_builds_actions_excluding_help():
38
+ help_command = HelpCommand(bot_name="Bot", bot_help_subtitle="Help", bot_help_image="https://example.com/image.png")
39
+ other_command = SimpleCommand(keyword="ping", help_message="Ping")
40
+ help_command.commands = {help_command, other_command}
41
+ actions, hints = help_command.build_actions_and_hints(thread_parent_id="thread-1")
42
+ assert len(actions) == 1
43
+ assert actions[0].data[COMMAND_KEYWORD_KEY] == "ping"
44
+ assert actions[0].data["thread_parent_id"] == "thread-1"
45
+ assert len(hints) == 1
46
+
47
+
48
+ def test_help_command_keyword_constant():
49
+ assert HELP_COMMAND_KEYWORD == "help"
@@ -0,0 +1,8 @@
1
+ from webex_bot.exceptions import BotException
2
+
3
+
4
+ def test_bot_exception_attributes():
5
+ exc = BotException("debug", "reply", reply_one_to_one=True)
6
+ assert exc.debug_message == "debug"
7
+ assert exc.reply_message == "reply"
8
+ assert exc.reply_one_to_one is True
@@ -0,0 +1,12 @@
1
+ from webex_bot.formatting import quote_info, quote_warning, quote_danger, code, html_link
2
+
3
+
4
+ def test_quote_helpers():
5
+ assert quote_info("hi") == "<blockquote class=info>hi</blockquote>"
6
+ assert quote_warning("hi") == "<blockquote class=warning>hi</blockquote>"
7
+ assert quote_danger("hi") == "<blockquote class=danger>hi</blockquote>"
8
+
9
+
10
+ def test_code_and_html_link():
11
+ assert code("x") == "<code>x</code>"
12
+ assert html_link("Label", "https://example.com") == "<a href='https://example.com'>Label</a>"
@@ -0,0 +1,32 @@
1
+ from webexpythonsdk.models.cards import AdaptiveCard
2
+
3
+ from webex_bot.models.response import Response, response_from_adaptive_card
4
+
5
+
6
+ def test_response_as_dict_filters_empty():
7
+ response = Response()
8
+ response.markdown = "hello"
9
+ response.files = "file-1"
10
+ response.attachments = {"contentType": "application/vnd.microsoft.card.adaptive", "content": {}}
11
+ result = response.as_dict()
12
+ assert result["markdown"] == "hello"
13
+ assert result["files"] == ["file-1"]
14
+ assert result["attachments"] == [
15
+ {"contentType": "application/vnd.microsoft.card.adaptive", "content": {}}
16
+ ]
17
+ assert "roomId" not in result
18
+
19
+
20
+ def test_response_from_adaptive_card_populates_attachment():
21
+ card = AdaptiveCard(body=[], actions=[])
22
+ response = response_from_adaptive_card(card)
23
+ assert response.text == "This bot requires a client which can render cards."
24
+ assert response.markdown == "This bot requires a client which can render cards."
25
+ assert response.attachments[0]["contentType"] == "application/vnd.microsoft.card.adaptive"
26
+
27
+
28
+ def test_response_json_and_html_property():
29
+ response = Response(attributes={"text": "hello"})
30
+ response.html = "<b>hi</b>"
31
+ assert response.html == "<b>hi</b>"
32
+ assert '"text": "hello"' in response.json()
@@ -0,0 +1,149 @@
1
+ import types
2
+
3
+ import pytest
4
+
5
+ from webex_bot.models.command import Command
6
+ from webex_bot.models.response import Response
7
+ from webex_bot.exceptions import BotException
8
+ from webex_bot.webex_bot import WebexBot
9
+
10
+
11
+ class DummyCommand(Command):
12
+ def __init__(self, command_keyword="ping", exact_match=False, card_callback_keyword=None):
13
+ super().__init__(
14
+ command_keyword=command_keyword,
15
+ exact_command_keyword_match=exact_match,
16
+ help_message="Ping command",
17
+ card_callback_keyword=card_callback_keyword,
18
+ )
19
+
20
+ def execute(self, message, attachment_actions, activity):
21
+ return "pong"
22
+
23
+
24
+ class PreExecuteCommand(Command):
25
+ def __init__(self, command_keyword="work", delete_previous_message=False):
26
+ super().__init__(
27
+ command_keyword=command_keyword,
28
+ help_message="Pre execute",
29
+ delete_previous_message=delete_previous_message,
30
+ )
31
+
32
+ def pre_execute(self, message, attachment_actions, activity):
33
+ return "Working"
34
+
35
+ def execute(self, message, attachment_actions, activity):
36
+ return "Done"
37
+
38
+
39
+ class ExceptionCommand(Command):
40
+ def __init__(self, command_keyword="fail"):
41
+ super().__init__(
42
+ command_keyword=command_keyword,
43
+ help_message="Failing command",
44
+ )
45
+
46
+ def pre_execute(self, message, attachment_actions, activity):
47
+ raise BotException("debug", "pre-reply", reply_one_to_one=True)
48
+
49
+ def execute(self, message, attachment_actions, activity):
50
+ raise BotException("debug", "reply", reply_one_to_one=False)
51
+
52
+
53
+ def test_get_message_passed_to_command():
54
+ assert WebexBot.get_message_passed_to_command("help", "help me") == " me"
55
+ assert WebexBot.get_message_passed_to_command("help", "hello") == "hello"
56
+
57
+
58
+ def test_check_user_approved_unrestricted(bot):
59
+ assert bot.check_user_approved("user@example.com", approved_rooms=[]) is True
60
+
61
+
62
+ def test_check_user_approved_domain(bot):
63
+ bot.approved_domains = ["example.com"]
64
+ assert bot.check_user_approved("user@example.com", approved_rooms=[]) is True
65
+ assert bot.check_user_approved("user@other.com", approved_rooms=[]) is False
66
+
67
+
68
+ def test_check_user_approved_room_membership(bot):
69
+ assert bot.check_user_approved("member@example.com", approved_rooms=["room-1"]) is True
70
+ assert bot.check_user_approved("outsider@example.com", approved_rooms=["room-1"]) is False
71
+
72
+
73
+ def test_add_command_duplicate_callback_keyword_raises(bot):
74
+ first = DummyCommand(command_keyword="first", card_callback_keyword="dup")
75
+ second = DummyCommand(command_keyword="second", card_callback_keyword="dup")
76
+ bot.add_command(first)
77
+ with pytest.raises(Exception):
78
+ bot.add_command(second)
79
+
80
+
81
+ def test_process_incoming_message_ignores_other_bot(bot, teams_message, one_on_one_activity):
82
+ bot.allow_bot_to_bot = False
83
+ teams_message.personEmail = "otherbot@example.com"
84
+ activity = dict(one_on_one_activity)
85
+ activity["actor"]["type"] = "BOT"
86
+ bot.process_incoming_message(teams_message, activity)
87
+ assert bot.teams.messages.created == []
88
+
89
+
90
+ def test_process_incoming_message_runs_help(bot, teams_message, one_on_one_activity):
91
+ teams_message.text = "unknown"
92
+ bot.process_incoming_message(teams_message, one_on_one_activity)
93
+ assert len(bot.teams.messages.created) >= 1
94
+
95
+
96
+ def test_do_reply_with_response_sets_room_id(bot):
97
+ response = Response()
98
+ response.markdown = "hi"
99
+ bot.do_reply(response, "room-1", "user@example.com", False, True, "thread-1")
100
+ assert bot.teams.messages.created[-1]["roomId"] == "room-1"
101
+
102
+
103
+ def test_do_reply_with_response_list(bot):
104
+ response = Response()
105
+ response.markdown = "hello"
106
+ bot.do_reply([response], "room-1", "user@example.com", False, True, "thread-1")
107
+ assert bot.teams.messages.created[-1]["roomId"] == "room-1"
108
+
109
+
110
+ def test_process_raw_command_exact_match(bot, teams_message, one_on_one_activity):
111
+ command = DummyCommand(command_keyword="ping", exact_match=True)
112
+ bot.add_command(command)
113
+ bot.process_raw_command("ping", teams_message, "user@example.com", one_on_one_activity)
114
+ assert bot.teams.messages.created[-1]["markdown"] == "pong"
115
+
116
+
117
+ def test_process_raw_command_callback(bot, one_on_one_activity):
118
+ command = DummyCommand(command_keyword="ping", card_callback_keyword="ping_cb")
119
+ bot.add_command(command)
120
+ attachment_actions = types.SimpleNamespace(
121
+ inputs={"callback_keyword": "ping_cb"},
122
+ roomId="room-1",
123
+ )
124
+ activity = dict(one_on_one_activity)
125
+ activity["actor"]["emailAddress"] = "user@example.com"
126
+ bot.process_incoming_card_action(attachment_actions, activity)
127
+ assert bot.teams.messages.created[-1]["markdown"] == "pong"
128
+
129
+
130
+ def test_process_raw_command_delete_previous_message(bot, teams_message, one_on_one_activity):
131
+ command = PreExecuteCommand(delete_previous_message=True)
132
+ bot.add_command(command)
133
+ bot.process_raw_command("work", teams_message, "user@example.com", one_on_one_activity)
134
+ assert teams_message.messageId in bot.teams.messages.deleted
135
+ assert "message-1" in bot.teams.messages.deleted
136
+
137
+
138
+ def test_run_pre_execute_handles_bot_exception(bot, teams_message, one_on_one_activity):
139
+ command = ExceptionCommand()
140
+ reply, one_to_one = bot.run_pre_execute(command, "msg", teams_message, one_on_one_activity)
141
+ assert reply == "pre-reply"
142
+ assert one_to_one is True
143
+
144
+
145
+ def test_run_command_and_handle_bot_exceptions(bot, teams_message, one_on_one_activity):
146
+ command = ExceptionCommand()
147
+ reply, one_to_one = bot.run_command_and_handle_bot_exceptions(command, "msg", teams_message, one_on_one_activity)
148
+ assert reply == "reply"
149
+ assert one_to_one is False
@@ -0,0 +1,37 @@
1
+ from webex_bot.websockets.webex_websocket_client import WebexWebsocketClient
2
+
3
+
4
+ def _make_client():
5
+ client = WebexWebsocketClient.__new__(WebexWebsocketClient)
6
+ client._get_headers = lambda: {"Authorization": "Bearer test"}
7
+ return client
8
+
9
+
10
+ def test_get_websocket_connect_kwargs_prefers_extra_headers():
11
+ def connect(*, extra_headers=None):
12
+ return extra_headers
13
+
14
+ client = _make_client()
15
+ assert client._get_websocket_connect_kwargs(connect) == {
16
+ "extra_headers": {"Authorization": "Bearer test"}
17
+ }
18
+
19
+
20
+ def test_get_websocket_connect_kwargs_uses_additional_headers():
21
+ def connect(*, additional_headers=None):
22
+ return additional_headers
23
+
24
+ client = _make_client()
25
+ assert client._get_websocket_connect_kwargs(connect) == {
26
+ "additional_headers": {"Authorization": "Bearer test"}
27
+ }
28
+
29
+
30
+ def test_get_websocket_connect_kwargs_fallback_to_extra_headers():
31
+ def connect(*, headers=None):
32
+ return headers
33
+
34
+ client = _make_client()
35
+ assert client._get_websocket_connect_kwargs(connect) == {
36
+ "extra_headers": {"Authorization": "Bearer test"}
37
+ }
@@ -1,4 +1,4 @@
1
1
  """Top-level package for Webex Bot."""
2
2
 
3
3
  __author__ = """Finbarr Brady"""
4
- __version__ = '1.0.8'
4
+ __version__ = '1.1.12'
@@ -17,6 +17,7 @@ class EchoCommand(Command):
17
17
  super().__init__(
18
18
  command_keyword="echo",
19
19
  help_message="Echo Words Back to You!",
20
+ delete_previous_message=True,
20
21
  chained_commands=[EchoCallback()])
21
22
 
22
23
  def pre_execute(self, message, attachment_actions, activity):
@@ -174,7 +174,7 @@ class WebexBot(WebexWebsocketClient):
174
174
  if member.personEmail == user_email:
175
175
  is_user_member = True
176
176
  except webexpythonsdk.exceptions.ApiError as apie:
177
- log.warn(f"API error: {apie}")
177
+ log.warning(f"API error: {apie}")
178
178
  return is_user_member
179
179
 
180
180
  def process_incoming_card_action(self, attachment_actions, activity):
@@ -308,6 +308,8 @@ class WebexBot(WebexWebsocketClient):
308
308
  log.info(f"delete_previous_message is True. Deleting message with ID: {previous_message_id}")
309
309
  self.teams.messages.delete(previous_message_id)
310
310
 
311
+ pre_reply_message_id = None
312
+
311
313
  if not is_card_callback_command and command.card is not None:
312
314
  response = Response()
313
315
  response.text = "This bot requires a client which can render cards."
@@ -320,7 +322,7 @@ class WebexBot(WebexWebsocketClient):
320
322
  message=message_without_command,
321
323
  teams_message=teams_message,
322
324
  activity=activity)
323
- self.do_reply(pre_card_load_reply, room_id, user_email, pre_card_load_reply_one_to_one, is_one_on_one_space, thread_parent_id)
325
+ pre_reply_message_id = self.do_reply(pre_card_load_reply, room_id, user_email, pre_card_load_reply_one_to_one, is_one_on_one_space, thread_parent_id)
324
326
  reply = response
325
327
  else:
326
328
  log.debug(f"Going to run command: '{command}' with input: '{message_without_command}'")
@@ -328,25 +330,36 @@ class WebexBot(WebexWebsocketClient):
328
330
  message=message_without_command,
329
331
  teams_message=teams_message,
330
332
  activity=activity)
331
- self.do_reply(pre_execute_reply, room_id, user_email, pre_execute_reply_one_to_one, is_one_on_one_space, thread_parent_id)
333
+ pre_reply_message_id = self.do_reply(pre_execute_reply, room_id, user_email, pre_execute_reply_one_to_one, is_one_on_one_space, thread_parent_id)
332
334
  reply, reply_one_to_one = self.run_command_and_handle_bot_exceptions(command=command,
333
335
  message=message_without_command,
334
336
  teams_message=teams_message,
335
337
  activity=activity)
336
338
  log.info(f"Using thread id={thread_parent_id}")
337
- return self.do_reply(reply, room_id, user_email, reply_one_to_one, is_one_on_one_space, thread_parent_id)
339
+ final_message_id = self.do_reply(reply, room_id, user_email, reply_one_to_one, is_one_on_one_space, thread_parent_id)
340
+
341
+ # If requested, delete the pre-execute (or pre-card-load) message once the final reply has been sent
342
+ if command.delete_previous_message and pre_reply_message_id:
343
+ try:
344
+ log.info(f"Deleting pre-execute message with ID: {pre_reply_message_id}")
345
+ self.teams.messages.delete(pre_reply_message_id)
346
+ except Exception as e:
347
+ log.warning(f"Failed to delete pre-execute message {pre_reply_message_id}: {e}")
348
+
349
+ return final_message_id
338
350
 
339
351
  def do_reply(self, reply, room_id, user_email, reply_one_to_one, is_one_on_one_space, conv_target_id):
340
352
  # allow command handlers to craft their own Teams message
353
+ created_message_id = None
341
354
  if reply and isinstance(reply, Response):
342
355
  # If the Response lacks a roomId, set it to the incoming room
343
356
  if not reply.roomId:
344
357
  reply.roomId = room_id
345
358
  if not reply.parentId and conv_target_id and self.threads:
346
359
  reply.parentId = conv_target_id
347
- reply = reply.as_dict()
348
- self.teams.messages.create(**reply)
349
- reply = "ok"
360
+ reply_dict = reply.as_dict()
361
+ created = self.teams.messages.create(**reply_dict)
362
+ created_message_id = getattr(created, 'id', None)
350
363
  # Support returning a list of Responses
351
364
  elif reply and (isinstance(reply, list) or isinstance(reply, types.GeneratorType)):
352
365
  for response in reply:
@@ -356,24 +369,24 @@ class WebexBot(WebexWebsocketClient):
356
369
  response.roomId = room_id
357
370
  if not response.parentId and conv_target_id:
358
371
  response.parentId = conv_target_id
359
- self.teams.messages.create(**response.as_dict())
372
+ created = self.teams.messages.create(**response.as_dict())
373
+ created_message_id = getattr(created, 'id', None)
360
374
  else:
361
375
  # Just a plain message
362
- self.send_message_to_room_or_person(user_email,
363
- room_id,
364
- reply_one_to_one,
365
- is_one_on_one_space,
366
- response,
367
- conv_target_id)
368
- reply = "ok"
376
+ created_message_id = self.send_message_to_room_or_person(user_email,
377
+ room_id,
378
+ reply_one_to_one,
379
+ is_one_on_one_space,
380
+ response,
381
+ conv_target_id)
369
382
  elif reply:
370
- self.send_message_to_room_or_person(user_email,
371
- room_id,
372
- reply_one_to_one,
373
- is_one_on_one_space,
374
- reply,
375
- conv_target_id)
376
- return reply
383
+ created_message_id = self.send_message_to_room_or_person(user_email,
384
+ room_id,
385
+ reply_one_to_one,
386
+ is_one_on_one_space,
387
+ reply,
388
+ conv_target_id)
389
+ return created_message_id
377
390
 
378
391
  def send_message_to_room_or_person(self,
379
392
  user_email,
@@ -384,27 +397,29 @@ class WebexBot(WebexWebsocketClient):
384
397
  conv_target_id):
385
398
  default_move_to_one_to_one_heads_up = \
386
399
  quote_info(f"{user_email} I've messaged you 1-1. Please reply to me there.")
400
+ last_created = None
387
401
  if reply_one_to_one:
388
402
  if not is_one_on_one_space:
389
403
  if self.threads:
390
- self.teams.messages.create(roomId=room_id,
391
- markdown=default_move_to_one_to_one_heads_up,
392
- parentId=conv_target_id)
404
+ last_created = self.teams.messages.create(roomId=room_id,
405
+ markdown=default_move_to_one_to_one_heads_up,
406
+ parentId=conv_target_id)
393
407
  else:
394
- self.teams.messages.create(roomId=room_id,
395
- markdown=default_move_to_one_to_one_heads_up)
408
+ last_created = self.teams.messages.create(roomId=room_id,
409
+ markdown=default_move_to_one_to_one_heads_up)
396
410
  if self.threads:
397
- self.teams.messages.create(toPersonEmail=user_email,
398
- markdown=reply,
399
- parentId=conv_target_id)
411
+ last_created = self.teams.messages.create(toPersonEmail=user_email,
412
+ markdown=reply,
413
+ parentId=conv_target_id)
400
414
  else:
401
- self.teams.messages.create(toPersonEmail=user_email,
402
- markdown=reply)
415
+ last_created = self.teams.messages.create(toPersonEmail=user_email,
416
+ markdown=reply)
403
417
  else:
404
418
  if self.threads:
405
- self.teams.messages.create(roomId=room_id, markdown=reply, parentId=conv_target_id)
419
+ last_created = self.teams.messages.create(roomId=room_id, markdown=reply, parentId=conv_target_id)
406
420
  else:
407
- self.teams.messages.create(roomId=room_id, markdown=reply)
421
+ last_created = self.teams.messages.create(roomId=room_id, markdown=reply)
422
+ return getattr(last_created, 'id', None)
408
423
 
409
424
  def run_pre_card_load_reply(self, command, message, teams_message, activity):
410
425
  """
@@ -413,7 +428,7 @@ class WebexBot(WebexWebsocketClient):
413
428
  try:
414
429
  return command.pre_card_load_reply(message, teams_message, activity), False
415
430
  except BotException as e:
416
- log.warn(f"BotException: {e.debug_message}")
431
+ log.warning(f"BotException: {e.debug_message}")
417
432
  return e.reply_message, e.reply_one_to_one
418
433
 
419
434
  def run_pre_execute(self, command, message, teams_message, activity):
@@ -423,14 +438,14 @@ class WebexBot(WebexWebsocketClient):
423
438
  try:
424
439
  return command.pre_execute(message, teams_message, activity), False
425
440
  except BotException as e:
426
- log.warn(f"BotException: {e.debug_message}")
441
+ log.warning(f"BotException: {e.debug_message}")
427
442
  return e.reply_message, e.reply_one_to_one
428
443
 
429
444
  def run_command_and_handle_bot_exceptions(self, command, message, teams_message, activity):
430
445
  try:
431
446
  return command.card_callback(message, teams_message, activity), False
432
447
  except BotException as e:
433
- log.warn(f"BotException: {e.debug_message}")
448
+ log.warning(f"BotException: {e.debug_message}")
434
449
  return e.reply_message, e.reply_one_to_one
435
450
 
436
451
  @staticmethod
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import inspect
3
4
  import logging
4
5
  import socket
5
6
  import ssl
@@ -10,7 +11,10 @@ import certifi
10
11
  import requests
11
12
  import websockets
12
13
  from webexpythonsdk import WebexAPI
13
- from websockets.exceptions import InvalidStatusCode
14
+ try:
15
+ from websockets import InvalidStatus
16
+ except ImportError: # pragma: no cover - fallback for older websockets versions
17
+ from websockets.exceptions import InvalidStatus
14
18
 
15
19
  from webex_bot import __version__
16
20
 
@@ -79,6 +83,20 @@ class WebexWebsocketClient(object):
79
83
  "trackingid": self.tracking_id
80
84
  }
81
85
 
86
+ def _get_websocket_connect_kwargs(self, connect_func):
87
+ headers = self._get_headers()
88
+ try:
89
+ params = inspect.signature(connect_func).parameters
90
+ except (TypeError, ValueError):
91
+ return {"extra_headers": headers}
92
+
93
+ if "extra_headers" in params:
94
+ return {"extra_headers": headers}
95
+ if "additional_headers" in params:
96
+ return {"additional_headers": headers}
97
+
98
+ return {"extra_headers": headers}
99
+
82
100
  def _process_incoming_websocket_message(self, msg):
83
101
  """
84
102
  Handle websocket data.
@@ -247,7 +265,7 @@ class WebexWebsocketClient(object):
247
265
  websockets.ConnectionClosedOK,
248
266
  websockets.ConnectionClosed,
249
267
  socket.gaierror,
250
- InvalidStatusCode,
268
+ InvalidStatus,
251
269
  ),
252
270
  max_time=MAX_BACKOFF_TIME
253
271
  )
@@ -258,14 +276,28 @@ class WebexWebsocketClient(object):
258
276
  if self.proxies and "wss" in self.proxies:
259
277
  logger.info(f"Using proxy for websocket connection: {self.proxies['wss']}")
260
278
  proxy = Proxy.from_url(self.proxies["wss"])
261
- connect = proxy_connect(ws_url, ssl=ssl_context, proxy=proxy, extra_headers=self._get_headers())
279
+ connect = proxy_connect(
280
+ ws_url,
281
+ ssl=ssl_context,
282
+ proxy=proxy,
283
+ **self._get_websocket_connect_kwargs(proxy_connect),
284
+ )
262
285
  elif self.proxies and "https" in self.proxies:
263
286
  logger.info(f"Using proxy for websocket connection: {self.proxies['https']}")
264
287
  proxy = Proxy.from_url(self.proxies["https"])
265
- connect = proxy_connect(ws_url, ssl=ssl_context, proxy=proxy, extra_headers=self._get_headers())
288
+ connect = proxy_connect(
289
+ ws_url,
290
+ ssl=ssl_context,
291
+ proxy=proxy,
292
+ **self._get_websocket_connect_kwargs(proxy_connect),
293
+ )
266
294
  else:
267
295
  logger.debug(f"Not using proxy for websocket connection.")
268
- connect = websockets.connect(ws_url, ssl=ssl_context, extra_headers=self._get_headers())
296
+ connect = websockets.connect(
297
+ ws_url,
298
+ ssl=ssl_context,
299
+ **self._get_websocket_connect_kwargs(websockets.connect),
300
+ )
269
301
 
270
302
  async with connect as _websocket:
271
303
  self.websocket = _websocket
@@ -287,10 +319,11 @@ class WebexWebsocketClient(object):
287
319
  asyncio.get_event_loop().run_until_complete(_connect_and_listen())
288
320
  # If we get here, the connection was successful, so break out of the loop
289
321
  break
290
- except InvalidStatusCode as e:
291
- logger.error(f"WebSocket handshake to {ws_url} failed with status {e.status_code}")
322
+ except InvalidStatus as e:
323
+ status_code = getattr(e.response, "status_code", None)
324
+ logger.error(f"WebSocket handshake to {ws_url} failed with status {status_code}")
292
325
 
293
- if e.status_code == 404:
326
+ if status_code == 404:
294
327
  current_404_retries += 1
295
328
  if current_404_retries >= max_404_retries:
296
329
  logger.error(f"Reached maximum retries ({max_404_retries}) for 404 errors. Giving up.")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: webex_bot
3
- Version: 1.0.8
3
+ Version: 1.1.12
4
4
  Summary: Python package for a Webex Bot based on websockets.
5
5
  Home-page: https://github.com/fbradyirl/webex_bot
6
6
  Author: Finbarr Brady
@@ -18,29 +18,16 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: webexpythonsdk==2.0.4
21
+ Requires-Dist: webexpythonsdk==2.0.5
22
22
  Requires-Dist: coloredlogs
23
- Requires-Dist: websockets==11.0.3
23
+ Requires-Dist: websockets==16
24
24
  Requires-Dist: backoff
25
25
  Provides-Extra: proxy
26
- Requires-Dist: websockets_proxy>=0.1.1; extra == "proxy"
27
- Dynamic: author
28
- Dynamic: author-email
29
- Dynamic: classifier
30
- Dynamic: description
31
- Dynamic: description-content-type
32
- Dynamic: home-page
33
- Dynamic: keywords
34
- Dynamic: license
35
- Dynamic: license-file
36
- Dynamic: provides-extra
37
- Dynamic: requires-dist
38
- Dynamic: requires-python
39
- Dynamic: summary
26
+ Requires-Dist: websockets_proxy>=0.1.3; extra == "proxy"
40
27
 
41
28
  # Introduction
42
29
 
43
- [![Pypi](https://img.shields.io/pypi/v/webex_bot.svg)](https://pypi.python.org/pypi/webex_bot) [![Build Status](https://github.com/fbradyirl/webex_bot/workflows/Python%20package/badge.svg)](https://github.com/fbradyirl/webex_bot/actions)
30
+ [![Pypi](https://img.shields.io/pypi/v/webex_bot.svg)](https://pypi.python.org/pypi/webex_bot) [![Build Status](https://github.com/fbradyirl/webex_bot/workflows/Python%20package/badge.svg)](https://github.com/fbradyirl/webex_bot/actions) [![CodeQL](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml/badge.svg)](https://github.com/fbradyirl/webex_bot/actions/workflows/codeql.yml) [![Coverage](https://codecov.io/gh/fbradyirl/webex_bot/branch/main/graph/badge.svg)](https://codecov.io/gh/fbradyirl/webex_bot) [![Release](https://img.shields.io/github/v/release/fbradyirl/webex_bot?sort=semver)](https://github.com/fbradyirl/webex_bot/releases) [![Downloads](https://img.shields.io/pypi/dm/webex_bot)](https://pypi.python.org/pypi/webex_bot)
44
31
 
45
32
  > [!IMPORTANT]
46
33
  > This repository is only sporadically maintained. Breaking API changes will be maintained on a best efforts basis.
@@ -50,7 +37,7 @@ Dynamic: summary
50
37
  > Bug reports unrelated to API changes may not get the attention you want.
51
38
 
52
39
 
53
- By using this module, you can create a [Webex Teams][5] messaging bot quickly in just a couple of lines of code.
40
+ By using this module, you can create a [Webex][5] messaging bot quickly in just a couple of lines of code.
54
41
 
55
42
  This module does not require you to set up an ngrok tunnel to receive incoming messages when behind a firewall or
56
43
  inside a LAN. This package instead uses a websocket to receive messages from the Webex cloud.
@@ -75,7 +62,7 @@ You can find a sample project, using OpenAI/ChatGPT with this library here: http
75
62
 
76
63
  ----
77
64
 
78
- **Only Python 3.13 is tested at this time.**
65
+ **Python 3.10, 3.11, and 3.12 are tested at this time.**
79
66
 
80
67
  1. Install this module from pypi:
81
68
 
@@ -113,7 +100,7 @@ proxies = {
113
100
  # Create a Bot Object
114
101
  bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN"),
115
102
  approved_rooms=['06586d8d-6aad-4201-9a69-0bf9eeb5766e'],
116
- bot_name="My Teams Ops Bot",
103
+ bot_name="My Webex Ops Bot",
117
104
  include_demo_commands=True,
118
105
  proxies=proxies)
119
106
 
@@ -464,6 +451,14 @@ bot = WebexBot(teams_bot_token=os.getenv("WEBEX_ACCESS_TOKEN")
464
451
 
465
452
  * Allow flag to disable bot to bot check
466
453
 
454
+ ### 1.1.6 (2026-Feb-02)
455
+
456
+ * Update dependancies.
457
+ * Expand test coverage and add CI matrix for Python 3.10–3.12
458
+ * Add coverage reporting and update contributor docs
459
+ * Refresh websocket status handling to avoid deprecations
460
+
461
+
467
462
  [1]: https://github.com/aaugustin/websockets
468
463
 
469
464
  [2]: https://github.com/WebexCommunity/WebexPythonSDK
@@ -12,7 +12,14 @@ docs/installation.rst
12
12
  docs/make.bat
13
13
  docs/usage.rst
14
14
  tests/__init__.py
15
+ tests/conftest.py
16
+ tests/test_command_model.py
17
+ tests/test_commands.py
18
+ tests/test_exceptions.py
19
+ tests/test_formatting.py
20
+ tests/test_response.py
15
21
  tests/test_webex_bot.py
22
+ tests/test_webex_websocket_client.py
16
23
  webex_bot/__init__.py
17
24
  webex_bot/exceptions.py
18
25
  webex_bot/formatting.py
@@ -0,0 +1,7 @@
1
+ webexpythonsdk==2.0.5
2
+ coloredlogs
3
+ websockets==16
4
+ backoff
5
+
6
+ [proxy]
7
+ websockets_proxy>=0.1.3
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- """Tests for `webex_bot` package."""
4
-
5
- import pytest
6
-
7
-
8
- @pytest.fixture
9
- def response():
10
- """Sample pytest fixture.
11
-
12
- See more at: http://doc.pytest.org/en/latest/fixture.html
13
- """
14
- # import requests
15
- # return requests.get('https://github.com/audreyr/cookiecutter-pypackage')
16
-
17
-
18
- def test_content(response):
19
- """Sample pytest test function with the pytest fixture as an argument."""
20
- # from bs4 import BeautifulSoup
21
- # assert 'GitHub' in BeautifulSoup(response.content).title.string
@@ -1,7 +0,0 @@
1
- webexpythonsdk==2.0.4
2
- coloredlogs
3
- websockets==11.0.3
4
- backoff
5
-
6
- [proxy]
7
- websockets_proxy>=0.1.1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes