iembot 0.3.0__tar.gz → 0.3.1__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 (54) hide show
  1. {iembot-0.3.0 → iembot-0.3.1}/.github/workflows/build.yml +1 -1
  2. {iembot-0.3.0 → iembot-0.3.1}/CHANGELOG.md +5 -1
  3. {iembot-0.3.0 → iembot-0.3.1}/PKG-INFO +3 -3
  4. {iembot-0.3.0 → iembot-0.3.1}/README.md +1 -1
  5. {iembot-0.3.0 → iembot-0.3.1}/environment.yml +2 -0
  6. iembot-0.3.1/pip_requirements.txt +4 -0
  7. {iembot-0.3.0 → iembot-0.3.1}/pyproject.toml +2 -2
  8. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/main.py +3 -5
  9. iembot-0.3.1/src/iembot/memcache.py +45 -0
  10. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/slack.py +42 -10
  11. {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/PKG-INFO +3 -3
  12. {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/SOURCES.txt +1 -0
  13. {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/requires.txt +1 -1
  14. {iembot-0.3.0 → iembot-0.3.1}/tests/test_main.py +2 -6
  15. {iembot-0.3.0 → iembot-0.3.1}/tests/test_slack.py +41 -0
  16. iembot-0.3.0/pip_requirements.txt +0 -4
  17. {iembot-0.3.0 → iembot-0.3.1}/.deepsource.toml +0 -0
  18. {iembot-0.3.0 → iembot-0.3.1}/.github/dependabot.yml +0 -0
  19. {iembot-0.3.0 → iembot-0.3.1}/.github/workflows/codeql.yml +0 -0
  20. {iembot-0.3.0 → iembot-0.3.1}/.github/workflows/etchosts.txt +0 -0
  21. {iembot-0.3.0 → iembot-0.3.1}/.gitignore +0 -0
  22. {iembot-0.3.0 → iembot-0.3.1}/.pre-commit-config.yaml +0 -0
  23. {iembot-0.3.0 → iembot-0.3.1}/LICENSE +0 -0
  24. {iembot-0.3.0 → iembot-0.3.1}/MANIFEST.in +0 -0
  25. {iembot-0.3.0 → iembot-0.3.1}/conda_requirements.txt +0 -0
  26. {iembot-0.3.0 → iembot-0.3.1}/conftest.py +0 -0
  27. {iembot-0.3.0 → iembot-0.3.1}/run.sh +0 -0
  28. {iembot-0.3.0 → iembot-0.3.1}/scripts/remove_twuser_oauth.py +0 -0
  29. {iembot-0.3.0 → iembot-0.3.1}/settings_example.json +0 -0
  30. {iembot-0.3.0 → iembot-0.3.1}/setup.cfg +0 -0
  31. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/__init__.py +0 -0
  32. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/atmosphere.py +0 -0
  33. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/bot.py +0 -0
  34. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/data/startrek +0 -0
  35. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/mastodon.py +0 -0
  36. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/msghandlers.py +0 -0
  37. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/twitter.py +0 -0
  38. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/types.py +0 -0
  39. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/util.py +0 -0
  40. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/webhooks.py +0 -0
  41. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/webservices.py +0 -0
  42. {iembot-0.3.0 → iembot-0.3.1}/src/iembot/xmpp.py +0 -0
  43. {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/dependency_links.txt +0 -0
  44. {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/entry_points.txt +0 -0
  45. {iembot-0.3.0 → iembot-0.3.1}/src/iembot.egg-info/top_level.txt +0 -0
  46. {iembot-0.3.0 → iembot-0.3.1}/tests/test_atmosphere.py +0 -0
  47. {iembot-0.3.0 → iembot-0.3.1}/tests/test_bot.py +0 -0
  48. {iembot-0.3.0 → iembot-0.3.1}/tests/test_init.py +0 -0
  49. {iembot-0.3.0 → iembot-0.3.1}/tests/test_mastodon.py +0 -0
  50. {iembot-0.3.0 → iembot-0.3.1}/tests/test_twitter.py +0 -0
  51. {iembot-0.3.0 → iembot-0.3.1}/tests/test_util.py +0 -0
  52. {iembot-0.3.0 → iembot-0.3.1}/tests/test_util_channels.py +0 -0
  53. {iembot-0.3.0 → iembot-0.3.1}/tests/test_webservices.py +0 -0
  54. {iembot-0.3.0 → iembot-0.3.1}/tests/test_xmpp.py +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  strategy:
16
16
  matrix:
17
- PYTHON_VERSION: ["3.10", "3.13", "3.14"]
17
+ PYTHON_VERSION: ["3.11", "3.13", "3.14"]
18
18
  env:
19
19
  PYTHON_VERSION: ${{ matrix.PYTHON_VERSION }}
20
20
  steps:
@@ -3,14 +3,18 @@
3
3
 
4
4
  All notable changes to this library are documented in this file.
5
5
 
6
- ## Unreleased Version
6
+ ## **0.3.1** (4 Feb 2026)
7
7
 
8
8
  ### API Changes
9
9
 
10
+ - Replace `txyam2` dependency with `pymemcache` called from a thread (#140).
11
+
10
12
  ### New Features
11
13
 
12
14
  ### Bug Fixes
13
15
 
16
+ - Correct slack subscription logic when a new channel is joined (#136).
17
+
14
18
  ## **0.3.0** (4 Feb 2026)
15
19
 
16
20
  First sane release of this code base onto pypi and hopefully conda-forge.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iembot
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A poorly written XMPP bot that does other things
5
5
  Author-email: daryl herzmann <akrherz@gmail.com>
6
6
  License: Apache
@@ -20,11 +20,11 @@ Requires-Dist: httpx
20
20
  Requires-Dist: mastodon-py
21
21
  Requires-Dist: psycopg
22
22
  Requires-Dist: pyiem>=1.26
23
+ Requires-Dist: pymemcache
23
24
  Requires-Dist: python-twitter
24
25
  Requires-Dist: requests
25
26
  Requires-Dist: service-identity
26
27
  Requires-Dist: twisted>=18.4
27
- Requires-Dist: txyam2
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: cartopy; extra == "dev"
30
30
  Requires-Dist: codecov; extra == "dev"
@@ -41,7 +41,7 @@ Dynamic: license-file
41
41
  [![Code Health](https://landscape.io/github/akrherz/iembot/master/landscape.svg?style=flat)](https://landscape.io/github/akrherz/iembot/master)
42
42
 
43
43
  I am a XMPP client with limited bot capabilities. In general, I am a message
44
- router more than anything.
44
+ router more than anything. Currently requires python 3.11+.
45
45
 
46
46
  ## Run iembot in development
47
47
 
@@ -5,7 +5,7 @@
5
5
  [![Code Health](https://landscape.io/github/akrherz/iembot/master/landscape.svg?style=flat)](https://landscape.io/github/akrherz/iembot/master)
6
6
 
7
7
  I am a XMPP client with limited bot capabilities. In general, I am a message
8
- router more than anything.
8
+ router more than anything. Currently requires python 3.11+.
9
9
 
10
10
  ## Run iembot in development
11
11
 
@@ -7,9 +7,11 @@ dependencies:
7
7
  - twisted>=18.4.0
8
8
  - psycopg
9
9
  - pyiem>=1.26
10
+ - pymemcache
10
11
  - pytest
11
12
  - pytest-cov
12
13
  - pytest-runner
14
+ - pytest-twisted
13
15
  # bot non-async twitter
14
16
  - python-twitter
15
17
  - service_identity
@@ -0,0 +1,4 @@
1
+ # generates RSS
2
+ feedgen
3
+ # memcached client
4
+ pymemcache
@@ -28,11 +28,11 @@ dependencies = [
28
28
  "mastodon-py",
29
29
  "psycopg",
30
30
  "pyiem>=1.26",
31
+ "pymemcache",
31
32
  "python-twitter",
32
33
  "requests",
33
34
  "service-identity",
34
35
  "twisted>=18.4",
35
- "txyam2",
36
36
  ]
37
37
 
38
38
  optional-dependencies.dev = [
@@ -57,7 +57,7 @@ include-package-data = true
57
57
  version_scheme = "post-release"
58
58
 
59
59
  [tool.ruff]
60
- target-version = "py310"
60
+ target-version = "py311"
61
61
 
62
62
  line-length = 79
63
63
  lint.select = [
@@ -18,10 +18,10 @@ from twisted.internet import reactor
18
18
  from twisted.python import log
19
19
  from twisted.python.logfile import DailyLogFile
20
20
  from twisted.web import server
21
- from txyam.client import YamClient
22
21
 
23
22
  from iembot import webservices
24
23
  from iembot.bot import JabberClient
24
+ from iembot.memcache import build_memcache_client
25
25
  from iembot.msghandlers import register_handler
26
26
 
27
27
 
@@ -43,10 +43,8 @@ def _build_dbpool(config: dict) -> adbapi.ConnectionPool:
43
43
  )
44
44
 
45
45
 
46
- def _build_memcache_client(memcache: str) -> YamClient:
47
- client = YamClient(reactor, [memcache])
48
- client.connect()
49
- return client
46
+ def _build_memcache_client(memcache: str):
47
+ return build_memcache_client(memcache)
50
48
 
51
49
 
52
50
  def _start_logging(logfile: str | None) -> None:
@@ -0,0 +1,45 @@
1
+ """Memcache client helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pymemcache.client.base import Client as MemcacheClient
6
+ from twisted.internet import threads
7
+
8
+
9
+ def parse_memcache_addr(memcache: str) -> tuple[str, int]:
10
+ """."""
11
+ memcache = memcache.removeprefix("tcp:")
12
+ host = memcache
13
+ port = 11211
14
+ if ":" in memcache:
15
+ host, port_str = memcache.rsplit(":", 1)
16
+ try:
17
+ port = int(port_str)
18
+ except ValueError:
19
+ host = memcache
20
+ if not host:
21
+ host = "localhost"
22
+ return host, port
23
+
24
+
25
+ class ThreadedMemcacheClient:
26
+ def __init__(self, host: str, port: int) -> None:
27
+ self._client = MemcacheClient((host, port))
28
+
29
+ def get(self, key: bytes):
30
+ def _get():
31
+ data = self._client.get(key)
32
+ return (0, data)
33
+
34
+ return threads.deferToThread(_get)
35
+
36
+ def disconnect(self) -> None:
37
+ try:
38
+ self._client.close()
39
+ except Exception:
40
+ pass
41
+
42
+
43
+ def build_memcache_client(memcache: str) -> ThreadedMemcacheClient:
44
+ host, port = parse_memcache_addr(memcache)
45
+ return ThreadedMemcacheClient(host, port)
@@ -91,22 +91,51 @@ class SlackSubscribeChannel(resource.Resource):
91
91
  def store_slack_subscription(
92
92
  self,
93
93
  txn,
94
- team_id,
95
- channel_id,
96
- subkey,
94
+ team_id: str,
95
+ channel_id: str,
96
+ subkey: str,
97
97
  ):
98
98
  """write to database."""
99
+ log.msg(
100
+ f"Handing slack subscription for T:`{team_id}` TC:`{channel_id}` "
101
+ f"C:`{subkey}`"
102
+ )
103
+ # Step 1, ensure that the slack channel exists
104
+ txn.execute(
105
+ """
106
+ select iembot_account_id from iembot_slack_team_channels
107
+ where team_id = %s and channel_id = %s
108
+ """,
109
+ (team_id, channel_id),
110
+ )
111
+ res = txn.fetchall()
112
+ if not res:
113
+ log.msg(
114
+ f"Slack Team: {team_id} Channel: {channel_id} does not exist "
115
+ "creating..."
116
+ )
117
+ txn.execute(
118
+ """
119
+ insert into iembot_slack_team_channels
120
+ (iembot_account_id, team_id, channel_id)
121
+ values (
122
+ (select create_iembot_account('slack')), %s, %s)
123
+ returning iembot_account_id
124
+ """,
125
+ (team_id, channel_id),
126
+ )
127
+ res = txn.fetchall()
128
+ iembot_account_id = res[0]["iembot_account_id"]
99
129
  txn.execute(
100
130
  """
101
131
  insert into iembot_subscriptions(iembot_account_id, channel_id)
102
132
  values (
103
- (select iembot_account_id from iembot_slack_team_channels
104
- where team_id = %s and channel_id = %s),
133
+ %s,
105
134
  (select get_or_create_iembot_channel_id(%s))
106
135
  )
107
136
  on conflict do nothing
108
137
  """,
109
- (team_id, channel_id, subkey),
138
+ (iembot_account_id, subkey),
110
139
  )
111
140
 
112
141
  def render(self, request):
@@ -120,11 +149,14 @@ class SlackSubscribeChannel(resource.Resource):
120
149
  channel_id,
121
150
  subkey,
122
151
  )
152
+ defer.addErrback(
153
+ lambda _: request.write(b"Error processing subscription")
154
+ )
123
155
  defer.addCallback(
124
156
  lambda _: request.write(f"Subscribed to {subkey}".encode("ascii"))
125
157
  )
126
- defer.addCallback(lambda _: request.finish())
127
- defer.addCallback(self.iembot.load_slack)
158
+ defer.addBoth(lambda _: request.finish())
159
+ defer.addCallback(lambda _: self.iembot.load_slack())
128
160
 
129
161
  return NOT_DONE_YET
130
162
 
@@ -173,7 +205,7 @@ class SlackUnsubscribeChannel(resource.Resource):
173
205
  )
174
206
  )
175
207
  defer.addCallback(lambda _: request.finish())
176
- defer.addCallback(self.iembot.load_slack)
208
+ defer.addCallback(lambda _: self.iembot.load_slack())
177
209
 
178
210
  return NOT_DONE_YET
179
211
 
@@ -249,7 +281,7 @@ class SlackOauthChannel(resource.Resource):
249
281
  self.do_request_in_thread, data
250
282
  )
251
283
  defer.addCallback(self._cb_oauth, request)
252
- defer.addCallback(self.iembot.load_slack)
284
+ defer.addCallback(lambda _: self.iembot.load_slack())
253
285
  defer.addErrback(self._eb_oauth, request)
254
286
  return NOT_DONE_YET
255
287
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iembot
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A poorly written XMPP bot that does other things
5
5
  Author-email: daryl herzmann <akrherz@gmail.com>
6
6
  License: Apache
@@ -20,11 +20,11 @@ Requires-Dist: httpx
20
20
  Requires-Dist: mastodon-py
21
21
  Requires-Dist: psycopg
22
22
  Requires-Dist: pyiem>=1.26
23
+ Requires-Dist: pymemcache
23
24
  Requires-Dist: python-twitter
24
25
  Requires-Dist: requests
25
26
  Requires-Dist: service-identity
26
27
  Requires-Dist: twisted>=18.4
27
- Requires-Dist: txyam2
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: cartopy; extra == "dev"
30
30
  Requires-Dist: codecov; extra == "dev"
@@ -41,7 +41,7 @@ Dynamic: license-file
41
41
  [![Code Health](https://landscape.io/github/akrherz/iembot/master/landscape.svg?style=flat)](https://landscape.io/github/akrherz/iembot/master)
42
42
 
43
43
  I am a XMPP client with limited bot capabilities. In general, I am a message
44
- router more than anything.
44
+ router more than anything. Currently requires python 3.11+.
45
45
 
46
46
  ## Run iembot in development
47
47
 
@@ -22,6 +22,7 @@ src/iembot/atmosphere.py
22
22
  src/iembot/bot.py
23
23
  src/iembot/main.py
24
24
  src/iembot/mastodon.py
25
+ src/iembot/memcache.py
25
26
  src/iembot/msghandlers.py
26
27
  src/iembot/slack.py
27
28
  src/iembot/twitter.py
@@ -5,11 +5,11 @@ httpx
5
5
  mastodon-py
6
6
  psycopg
7
7
  pyiem>=1.26
8
+ pymemcache
8
9
  python-twitter
9
10
  requests
10
11
  service-identity
11
12
  twisted>=18.4
12
- txyam2
13
13
 
14
14
  [dev]
15
15
  cartopy
@@ -80,13 +80,9 @@ def test_build_dbpool(monkeypatch):
80
80
  assert main_mod._build_dbpool(settings) is fakepool
81
81
 
82
82
 
83
- def test_build_memcache_client(monkeypatch):
84
- fake = mock.Mock()
85
- monkeypatch.setattr(main_mod, "YamClient", lambda _reactor, _addrs: fake)
86
- fake.connect = mock.Mock()
83
+ def test_build_memcache_client():
87
84
  result = main_mod._build_memcache_client("tcp:foo:1234")
88
- assert result is fake
89
- fake.connect.assert_called_once()
85
+ assert result
90
86
 
91
87
 
92
88
  def test_fatal_stops_reactor(monkeypatch):
@@ -3,6 +3,9 @@
3
3
  from unittest import mock
4
4
 
5
5
  import pytest
6
+ from twisted.internet import defer
7
+ from twisted.web.server import NOT_DONE_YET
8
+ from twisted.web.test.requesthelper import DummyRequest
6
9
 
7
10
  from iembot.slack import (
8
11
  SlackInstallChannel,
@@ -14,6 +17,32 @@ from iembot.slack import (
14
17
  from iembot.types import JabberClient
15
18
 
16
19
 
20
+ @defer.inlineCallbacks
21
+ def test_subscribe_channel_render(bot: JabberClient):
22
+ """Test that we can run the render workflow."""
23
+ ss = SlackSubscribeChannel(bot)
24
+ request = DummyRequest([])
25
+ request.args = {
26
+ b"team_id": [b"T12345"],
27
+ b"channel_id": [b"C67890"],
28
+ b"text": [b"AFDDMX"],
29
+ }
30
+ bot.dbpool.runInteraction = mock.Mock(return_value=defer.succeed(None))
31
+ result = ss.render(request)
32
+ assert result == NOT_DONE_YET
33
+ yield defer.succeed(None)
34
+ assert b"Subscribed" in b"".join(request.written)
35
+
36
+
37
+ def test_install_channel_render(bot: JabberClient):
38
+ """See if we can render the install channel."""
39
+ ss = SlackInstallChannel(bot)
40
+ request = DummyRequest([b""])
41
+ result = ss.render(request)
42
+ assert result is not None
43
+ assert request.responseCode == 302
44
+
45
+
17
46
  @pytest.mark.parametrize("database", ["iembot"])
18
47
  def test_subscribe_channel(dbcursor, bot):
19
48
  """See if we can subscribe?"""
@@ -26,6 +55,18 @@ def test_subscribe_channel(dbcursor, bot):
26
55
  )
27
56
 
28
57
 
58
+ @pytest.mark.parametrize("database", ["iembot"])
59
+ def test_gh136_slack_first_channel_subscription(dbcursor, bot):
60
+ """See if subscribe works when we need to create a slack channel."""
61
+ ss = SlackSubscribeChannel(bot)
62
+ ss.store_slack_subscription(
63
+ dbcursor,
64
+ "TSS", # needs to exist in iembot_slack_teams
65
+ "CSD2", # new channel on slack unknown to the bot so far
66
+ "AFDDMX",
67
+ )
68
+
69
+
29
70
  @pytest.mark.parametrize("database", ["iembot"])
30
71
  def test_load_from_db(dbcursor, bot: JabberClient):
31
72
  """Exercise."""
@@ -1,4 +0,0 @@
1
- # generates RSS
2
- feedgen
3
- # twisted memcached
4
- txyam2
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