python-twitchchat 1.0.0__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.
@@ -0,0 +1,68 @@
1
+ test.py
2
+ run.bat
3
+ *.DS_Store
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+
8
+ # vim swp files
9
+ .*.swp
10
+
11
+ # C extensions
12
+ *.so
13
+
14
+ # Distribution / packaging
15
+ .Python
16
+ env/
17
+ build/
18
+ develop-eggs/
19
+ dist/
20
+ downloads/
21
+ eggs/
22
+ .eggs/
23
+ lib/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *,cover
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+
59
+ # Sphinx documentation
60
+ docs/_build/
61
+
62
+ # PyBuilder
63
+ target/
64
+
65
+ # uv / tooling
66
+ .venv/
67
+ .ruff_cache/
68
+ .pytest_cache/
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-twitchchat
3
+ Version: 1.0.0
4
+ Summary: Asyncio client for Twitch chat (IRC): typed events, callbacks and async iteration.
5
+ Project-URL: Homepage, https://github.com/shughes-uk/python-twitchchat
6
+ Author: shughes-uk
7
+ Keywords: asyncio,chat,irc,twitch
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.15
14
+ Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.15
17
+ Description-Content-Type: text/markdown
18
+
19
+ # python-twitchchat
20
+
21
+ An asyncio client for [Twitch chat over IRC](https://dev.twitch.tv/docs/chat/irc/).
22
+ It has no runtime dependencies and gives you typed events, an async iterator,
23
+ callback registration, rate-limited sending, automatic PING/PONG and automatic
24
+ reconnects.
25
+
26
+ - **Requirements:** Python 3.15 or newer, standard library only.
27
+ - **Install:** `pip install python-twitchchat` or `uv add python-twitchchat`. The import name is `twitchchat`.
28
+ - Connects to `irc.chat.twitch.tv:6697` over TLS.
29
+
30
+ ## Usage
31
+
32
+ ### Reading chat anonymously
33
+
34
+ If you don't pass a token, the client logs in as `justinfanNNNNN`. That's
35
+ enough to read chat, but not to send.
36
+
37
+ ```python
38
+ import asyncio
39
+
40
+ from twitchchat import ChatMessage, TwitchChat, UserNotice
41
+
42
+
43
+ async def main() -> None:
44
+ async with TwitchChat(["somechannel", "#another"]) as chat:
45
+ async for event in chat: # every event; or chat.events(ChatMessage, UserNotice)
46
+ match event:
47
+ case ChatMessage():
48
+ print(f"[#{event.channel}] <{event.display_name}> {event.text}")
49
+ case UserNotice(): # sub, resub, subgift, raid, announcement, ...
50
+ print(f"[#{event.channel}] {event.msg_id}: {event.system_msg}")
51
+
52
+
53
+ asyncio.run(main())
54
+ ```
55
+
56
+ ### Callbacks and sending messages
57
+
58
+ ```python
59
+ import asyncio
60
+
61
+ from twitchchat import ChatMessage, TwitchChat, UserNotice
62
+
63
+
64
+ async def main() -> None:
65
+ chat = TwitchChat(["mychannel"], nick="mybot", token="oauth-token-with-chat:read-chat:edit")
66
+
67
+ @chat.on(ChatMessage)
68
+ async def on_message(msg: ChatMessage) -> None:
69
+ if msg.text == "!ping":
70
+ await chat.reply(msg, "pong") # threaded reply
71
+
72
+ @chat.on(UserNotice)
73
+ def on_sub(notice: UserNotice) -> None: # plain functions work too
74
+ if notice.msg_id in {"sub", "resub"}:
75
+ print(notice.login, notice.params.get("cumulative-months"))
76
+
77
+ async with chat:
78
+ await chat.send("mychannel", "hello chat")
79
+ await chat.wait_closed() # run until closed or a fatal error
80
+
81
+
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ### API overview
86
+
87
+ `TwitchChat(channels, *, nick=None, token=None, reconnect=True, message_rate=(20, 30.0), join_rate=(20, 10.0), ...)`
88
+
89
+ | Member | Purpose |
90
+ | --- | --- |
91
+ | `async with chat` / `await chat.connect()` / `await chat.close()` | Connect and authenticate, then join the configured channels. A bad token raises `AuthenticationError`. |
92
+ | `async for event in chat` / `chat.events(*types, maxsize=10_000)` | Async iterator of events, optionally filtered by type. It buffers from the moment it is created (up to `maxsize` events; the oldest are dropped if you fall behind) and ends when the client closes or you `aclose()` it. |
93
+ | `chat.on(EventType)` / `add_listener` / `remove_listener` | Register sync or async callbacks. They also match subclasses, so `chat.on(Event)` receives everything. |
94
+ | `await chat.send(channel, text, reply_to=None)` / `await chat.reply(msg, text)` | Send a message, rate limited client-side. Anonymous sessions raise `TwitchChatError`. |
95
+ | `await chat.join(channel)` / `await chat.part(channel)` | Change channels at runtime. Joined channels are rejoined after a reconnect. |
96
+ | `await chat.send_raw(line)` | Send a raw IRC line. |
97
+ | `await chat.wait_closed()` | Wait for the client to close, re-raising any fatal error. |
98
+
99
+ **Events** are frozen dataclasses. Each one has `.raw` (the parsed `IrcMessage`) and `.tags`:
100
+
101
+ | Event | IRC message |
102
+ | --- | --- |
103
+ | `ChatMessage` | `PRIVMSG` |
104
+ | `UserNotice` | `USERNOTICE`: subs, raids, announcements and similar |
105
+ | `Join`, `Part` | `JOIN`, `PART` |
106
+ | `Notice` | `NOTICE` |
107
+ | `ClearChat`, `ClearMsg` | `CLEARCHAT`, `CLEARMSG` |
108
+ | `RoomState`, `UserState`, `GlobalUserState` | `ROOMSTATE`, `USERSTATE`, `GLOBALUSERSTATE` |
109
+ | `Whisper` | `WHISPER` |
110
+ | `Reconnect` | `RECONNECT` (the client reconnects on its own) |
111
+ | `Connected` | `001`, emitted on every (re)connect |
112
+ | `RawEvent` | Anything else |
113
+
114
+ The low-level parser is public: `twitchchat.parse_line()` and `format_line()`
115
+ handle IRCv3 tags, including escaped values such as `\s`, `\:` and `\\`.
116
+
117
+ ### Migrating from 0.1
118
+
119
+ The old thread-based `twitch_chat(user, oauth, channels, client_id)` class has
120
+ been replaced:
121
+
122
+ | Old | New |
123
+ | --- | --- |
124
+ | `subscribeChatMessage(cb)` | `chat.on(ChatMessage)(cb)` |
125
+ | `subscribeUsernotice(cb)` | `chat.on(UserNotice)(cb)` |
126
+ | `send_message(channel, msg)` | `await chat.send(channel, msg)` |
127
+ | `start()` / `join()` / `stop()` | `async with` / `wait_closed()` / `close()` |
128
+
129
+ Callbacks now get typed dataclasses instead of dicts of raw tags; raw tags are
130
+ still available as `event.tags`. The unused `client_id` argument has been
131
+ dropped, because the library makes no HTTP API calls.
132
+
133
+ ## Twitch platform notes (checked October 2026)
134
+
135
+ - **IRC still works, but Twitch recommends EventSub.** The product lifecycle
136
+ page lists Chat (IRC) as active, and no shutdown date has been announced.
137
+ Twitch's [IRC migration guide](https://dev.twitch.tv/docs/chat/irc-migration/)
138
+ recommends that new bots read chat through EventSub (`channel.chat.message`)
139
+ and send through the Helix *Send Chat Message* API. Some newer features only
140
+ exist there.
141
+ - **Endpoints:** use `irc.chat.twitch.tv:6697` (TLS) or `wss://irc-ws.chat.twitch.tv:443`.
142
+ Non-TLS WebSocket connections were decommissioned on 2025-08-15. The old code
143
+ used plaintext port 6667; this version uses TLS by default.
144
+ - **Auth:** `PASS oauth:<user access token>` followed by `NICK <login>`. Reading
145
+ needs the `chat:read` scope and sending needs `chat:edit`. You can get a token
146
+ through any OAuth flow, for example the
147
+ [device code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow).
148
+ Anonymous `justinfan` logins aren't documented, but they still work for
149
+ reading; this was verified live.
150
+ - **Rate limits:** normal accounts get 20 messages per 30 seconds, plus 1 message
151
+ per second per channel. Where you are mod, VIP or broadcaster the limit is 100
152
+ per 30 seconds, so pass `message_rate=(100, 30.0)`. Joins are limited to 20 per
153
+ 10 seconds. Going over the message limit can get your messages ignored for an
154
+ hour. The client limits itself to the normal-user defaults.
155
+ - **Kraken (v5) is gone:** it was shut down in February 2023. This library never
156
+ needed it, and the old `client_id` parameter did nothing, so it was removed.
157
+ Channel hosting (`HOSTTARGET`) was also removed by Twitch.
158
+
159
+ ## Example
160
+
161
+ [`examples/watch_chat.py`](examples/watch_chat.py) watches channels anonymously
162
+ and prints parsed events. It exits non-zero if no chat message arrives within
163
+ the time limit.
164
+
165
+ ```sh
166
+ uv run examples/watch_chat.py --seconds 60 somebigchannel anotherone
167
+ ```
168
+
169
+ ## Development
170
+
171
+ The project uses [uv](https://docs.astral.sh/uv/). `.python-version` pins
172
+ Python 3.15, which uv downloads automatically.
173
+
174
+ ```sh
175
+ uv sync # create .venv with dev tools (ruff, ty, pytest, pytest-asyncio)
176
+ uv run ruff check # lint
177
+ uv run ruff format # format (use --check in CI)
178
+ uv run ty check # type check
179
+ uv run pytest # unit tests (parser + client against a local fake IRC server)
180
+ ```
181
+
182
+ CI (`.github/workflows/ci.yml`) runs all four checks on every push and pull
183
+ request. Tests run on Linux, Windows and macOS.
184
+
185
+ ## Releasing
186
+
187
+ The version comes from the git tag through `hatch-vcs`, so there is no version
188
+ number to edit by hand.
189
+
190
+ 1. One-time setup on PyPI: go to *Your projects → Publishing → Add a new pending
191
+ publisher* (or the project's *Settings → Publishing* once it exists). Choose
192
+ GitHub and fill in:
193
+ - owner `shughes-uk`
194
+ - repository `python-twitchchat`
195
+ - workflow `release.yml`
196
+ - environment `pypi`
197
+
198
+ Then create an environment named `pypi` in the GitHub repository settings.
199
+ You can add required reviewers to it if you want manual approval before each
200
+ publish.
201
+ 2. Tag and push:
202
+ ```sh
203
+ git tag v2.0.0
204
+ git push origin v2.0.0
205
+ ```
206
+ 3. `.github/workflows/release.yml` then:
207
+ - runs CI
208
+ - runs `uv build` and checks the built version matches the tag
209
+ - publishes to PyPI with trusted publishing (no API token needed)
210
+ - creates a GitHub Release with generated notes and the sdist and wheel attached
211
+
212
+ Tags such as `v2.1.0rc1` are marked as pre-releases.
213
+
214
+ Note that the distribution name is **`python-twitchchat`**, because `twitchchat`
215
+ is already taken on PyPI by an unrelated project.
@@ -0,0 +1,197 @@
1
+ # python-twitchchat
2
+
3
+ An asyncio client for [Twitch chat over IRC](https://dev.twitch.tv/docs/chat/irc/).
4
+ It has no runtime dependencies and gives you typed events, an async iterator,
5
+ callback registration, rate-limited sending, automatic PING/PONG and automatic
6
+ reconnects.
7
+
8
+ - **Requirements:** Python 3.15 or newer, standard library only.
9
+ - **Install:** `pip install python-twitchchat` or `uv add python-twitchchat`. The import name is `twitchchat`.
10
+ - Connects to `irc.chat.twitch.tv:6697` over TLS.
11
+
12
+ ## Usage
13
+
14
+ ### Reading chat anonymously
15
+
16
+ If you don't pass a token, the client logs in as `justinfanNNNNN`. That's
17
+ enough to read chat, but not to send.
18
+
19
+ ```python
20
+ import asyncio
21
+
22
+ from twitchchat import ChatMessage, TwitchChat, UserNotice
23
+
24
+
25
+ async def main() -> None:
26
+ async with TwitchChat(["somechannel", "#another"]) as chat:
27
+ async for event in chat: # every event; or chat.events(ChatMessage, UserNotice)
28
+ match event:
29
+ case ChatMessage():
30
+ print(f"[#{event.channel}] <{event.display_name}> {event.text}")
31
+ case UserNotice(): # sub, resub, subgift, raid, announcement, ...
32
+ print(f"[#{event.channel}] {event.msg_id}: {event.system_msg}")
33
+
34
+
35
+ asyncio.run(main())
36
+ ```
37
+
38
+ ### Callbacks and sending messages
39
+
40
+ ```python
41
+ import asyncio
42
+
43
+ from twitchchat import ChatMessage, TwitchChat, UserNotice
44
+
45
+
46
+ async def main() -> None:
47
+ chat = TwitchChat(["mychannel"], nick="mybot", token="oauth-token-with-chat:read-chat:edit")
48
+
49
+ @chat.on(ChatMessage)
50
+ async def on_message(msg: ChatMessage) -> None:
51
+ if msg.text == "!ping":
52
+ await chat.reply(msg, "pong") # threaded reply
53
+
54
+ @chat.on(UserNotice)
55
+ def on_sub(notice: UserNotice) -> None: # plain functions work too
56
+ if notice.msg_id in {"sub", "resub"}:
57
+ print(notice.login, notice.params.get("cumulative-months"))
58
+
59
+ async with chat:
60
+ await chat.send("mychannel", "hello chat")
61
+ await chat.wait_closed() # run until closed or a fatal error
62
+
63
+
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ ### API overview
68
+
69
+ `TwitchChat(channels, *, nick=None, token=None, reconnect=True, message_rate=(20, 30.0), join_rate=(20, 10.0), ...)`
70
+
71
+ | Member | Purpose |
72
+ | --- | --- |
73
+ | `async with chat` / `await chat.connect()` / `await chat.close()` | Connect and authenticate, then join the configured channels. A bad token raises `AuthenticationError`. |
74
+ | `async for event in chat` / `chat.events(*types, maxsize=10_000)` | Async iterator of events, optionally filtered by type. It buffers from the moment it is created (up to `maxsize` events; the oldest are dropped if you fall behind) and ends when the client closes or you `aclose()` it. |
75
+ | `chat.on(EventType)` / `add_listener` / `remove_listener` | Register sync or async callbacks. They also match subclasses, so `chat.on(Event)` receives everything. |
76
+ | `await chat.send(channel, text, reply_to=None)` / `await chat.reply(msg, text)` | Send a message, rate limited client-side. Anonymous sessions raise `TwitchChatError`. |
77
+ | `await chat.join(channel)` / `await chat.part(channel)` | Change channels at runtime. Joined channels are rejoined after a reconnect. |
78
+ | `await chat.send_raw(line)` | Send a raw IRC line. |
79
+ | `await chat.wait_closed()` | Wait for the client to close, re-raising any fatal error. |
80
+
81
+ **Events** are frozen dataclasses. Each one has `.raw` (the parsed `IrcMessage`) and `.tags`:
82
+
83
+ | Event | IRC message |
84
+ | --- | --- |
85
+ | `ChatMessage` | `PRIVMSG` |
86
+ | `UserNotice` | `USERNOTICE`: subs, raids, announcements and similar |
87
+ | `Join`, `Part` | `JOIN`, `PART` |
88
+ | `Notice` | `NOTICE` |
89
+ | `ClearChat`, `ClearMsg` | `CLEARCHAT`, `CLEARMSG` |
90
+ | `RoomState`, `UserState`, `GlobalUserState` | `ROOMSTATE`, `USERSTATE`, `GLOBALUSERSTATE` |
91
+ | `Whisper` | `WHISPER` |
92
+ | `Reconnect` | `RECONNECT` (the client reconnects on its own) |
93
+ | `Connected` | `001`, emitted on every (re)connect |
94
+ | `RawEvent` | Anything else |
95
+
96
+ The low-level parser is public: `twitchchat.parse_line()` and `format_line()`
97
+ handle IRCv3 tags, including escaped values such as `\s`, `\:` and `\\`.
98
+
99
+ ### Migrating from 0.1
100
+
101
+ The old thread-based `twitch_chat(user, oauth, channels, client_id)` class has
102
+ been replaced:
103
+
104
+ | Old | New |
105
+ | --- | --- |
106
+ | `subscribeChatMessage(cb)` | `chat.on(ChatMessage)(cb)` |
107
+ | `subscribeUsernotice(cb)` | `chat.on(UserNotice)(cb)` |
108
+ | `send_message(channel, msg)` | `await chat.send(channel, msg)` |
109
+ | `start()` / `join()` / `stop()` | `async with` / `wait_closed()` / `close()` |
110
+
111
+ Callbacks now get typed dataclasses instead of dicts of raw tags; raw tags are
112
+ still available as `event.tags`. The unused `client_id` argument has been
113
+ dropped, because the library makes no HTTP API calls.
114
+
115
+ ## Twitch platform notes (checked October 2026)
116
+
117
+ - **IRC still works, but Twitch recommends EventSub.** The product lifecycle
118
+ page lists Chat (IRC) as active, and no shutdown date has been announced.
119
+ Twitch's [IRC migration guide](https://dev.twitch.tv/docs/chat/irc-migration/)
120
+ recommends that new bots read chat through EventSub (`channel.chat.message`)
121
+ and send through the Helix *Send Chat Message* API. Some newer features only
122
+ exist there.
123
+ - **Endpoints:** use `irc.chat.twitch.tv:6697` (TLS) or `wss://irc-ws.chat.twitch.tv:443`.
124
+ Non-TLS WebSocket connections were decommissioned on 2025-08-15. The old code
125
+ used plaintext port 6667; this version uses TLS by default.
126
+ - **Auth:** `PASS oauth:<user access token>` followed by `NICK <login>`. Reading
127
+ needs the `chat:read` scope and sending needs `chat:edit`. You can get a token
128
+ through any OAuth flow, for example the
129
+ [device code flow](https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#device-code-grant-flow).
130
+ Anonymous `justinfan` logins aren't documented, but they still work for
131
+ reading; this was verified live.
132
+ - **Rate limits:** normal accounts get 20 messages per 30 seconds, plus 1 message
133
+ per second per channel. Where you are mod, VIP or broadcaster the limit is 100
134
+ per 30 seconds, so pass `message_rate=(100, 30.0)`. Joins are limited to 20 per
135
+ 10 seconds. Going over the message limit can get your messages ignored for an
136
+ hour. The client limits itself to the normal-user defaults.
137
+ - **Kraken (v5) is gone:** it was shut down in February 2023. This library never
138
+ needed it, and the old `client_id` parameter did nothing, so it was removed.
139
+ Channel hosting (`HOSTTARGET`) was also removed by Twitch.
140
+
141
+ ## Example
142
+
143
+ [`examples/watch_chat.py`](examples/watch_chat.py) watches channels anonymously
144
+ and prints parsed events. It exits non-zero if no chat message arrives within
145
+ the time limit.
146
+
147
+ ```sh
148
+ uv run examples/watch_chat.py --seconds 60 somebigchannel anotherone
149
+ ```
150
+
151
+ ## Development
152
+
153
+ The project uses [uv](https://docs.astral.sh/uv/). `.python-version` pins
154
+ Python 3.15, which uv downloads automatically.
155
+
156
+ ```sh
157
+ uv sync # create .venv with dev tools (ruff, ty, pytest, pytest-asyncio)
158
+ uv run ruff check # lint
159
+ uv run ruff format # format (use --check in CI)
160
+ uv run ty check # type check
161
+ uv run pytest # unit tests (parser + client against a local fake IRC server)
162
+ ```
163
+
164
+ CI (`.github/workflows/ci.yml`) runs all four checks on every push and pull
165
+ request. Tests run on Linux, Windows and macOS.
166
+
167
+ ## Releasing
168
+
169
+ The version comes from the git tag through `hatch-vcs`, so there is no version
170
+ number to edit by hand.
171
+
172
+ 1. One-time setup on PyPI: go to *Your projects → Publishing → Add a new pending
173
+ publisher* (or the project's *Settings → Publishing* once it exists). Choose
174
+ GitHub and fill in:
175
+ - owner `shughes-uk`
176
+ - repository `python-twitchchat`
177
+ - workflow `release.yml`
178
+ - environment `pypi`
179
+
180
+ Then create an environment named `pypi` in the GitHub repository settings.
181
+ You can add required reviewers to it if you want manual approval before each
182
+ publish.
183
+ 2. Tag and push:
184
+ ```sh
185
+ git tag v2.0.0
186
+ git push origin v2.0.0
187
+ ```
188
+ 3. `.github/workflows/release.yml` then:
189
+ - runs CI
190
+ - runs `uv build` and checks the built version matches the tag
191
+ - publishes to PyPI with trusted publishing (no API token needed)
192
+ - creates a GitHub Release with generated notes and the sdist and wheel attached
193
+
194
+ Tags such as `v2.1.0rc1` are marked as pre-releases.
195
+
196
+ Note that the distribution name is **`python-twitchchat`**, because `twitchchat`
197
+ is already taken on PyPI by an unrelated project.
@@ -0,0 +1,68 @@
1
+ """Anonymously watch one or more Twitch channels and print parsed events.
2
+
3
+ Usage::
4
+
5
+ uv run examples/watch_chat.py [--seconds 60] channel [channel ...]
6
+
7
+ Exits non-zero if no chat message was received within the time limit.
8
+ Read-only: anonymous (justinfan) connections cannot send messages.
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import logging
14
+ import sys
15
+
16
+ from twitchchat import ChatMessage, Join, Notice, RoomState, TwitchChat, UserNotice
17
+
18
+
19
+ async def watch(channels: list[str], seconds: float) -> int:
20
+ """Print events for ``seconds`` and return the number of chat messages seen."""
21
+ count = 0
22
+ async with TwitchChat(channels) as chat:
23
+ print(f"connected as {chat.nick}, joining {', '.join(channels)}")
24
+
25
+ @chat.on(UserNotice)
26
+ def on_usernotice(event: UserNotice) -> None:
27
+ print(f"[#{event.channel}] *** {event.msg_id}: {event.system_msg} {event.params}")
28
+
29
+ try:
30
+ async with asyncio.timeout(seconds):
31
+ async for event in chat.events(ChatMessage, Join, Notice, RoomState):
32
+ match event:
33
+ case ChatMessage():
34
+ count += 1
35
+ ts = event.timestamp.strftime("%H:%M:%S") if event.timestamp else "?"
36
+ badges = ",".join(event.badges)
37
+ print(
38
+ f"[#{event.channel} {ts}] <{event.display_name}"
39
+ f"{' [' + badges + ']' if badges else ''}> {event.text!r}"
40
+ )
41
+ case Join(login=login) if login == chat.nick:
42
+ print(f"joined #{event.channel}")
43
+ case RoomState():
44
+ print(f"roomstate #{event.channel}: {event.tags}")
45
+ case Notice():
46
+ print(f"notice: {event.text}")
47
+ case _:
48
+ pass
49
+ except TimeoutError:
50
+ pass
51
+ return count
52
+
53
+
54
+ def main() -> None:
55
+ """Command line entry point."""
56
+ parser = argparse.ArgumentParser(description=__doc__)
57
+ parser.add_argument("channels", nargs="+")
58
+ parser.add_argument("--seconds", type=float, default=60.0)
59
+ parser.add_argument("--debug", action="store_true")
60
+ args = parser.parse_args()
61
+ logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
62
+ count = asyncio.run(watch(args.channels, args.seconds))
63
+ print(f"received {count} chat messages in {args.seconds:.0f}s")
64
+ sys.exit(0 if count else 1)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
@@ -0,0 +1,89 @@
1
+ [project]
2
+ name = "python-twitchchat"
3
+ dynamic = ["version"]
4
+ description = "Asyncio client for Twitch chat (IRC): typed events, callbacks and async iteration."
5
+ readme = "README.md"
6
+ authors = [{ name = "shughes-uk" }]
7
+ requires-python = ">=3.15"
8
+ dependencies = []
9
+ keywords = ["twitch", "irc", "chat", "asyncio"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Framework :: AsyncIO",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.15",
17
+ "Topic :: Communications :: Chat :: Internet Relay Chat",
18
+ "Typing :: Typed",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/shughes-uk/python-twitchchat"
23
+
24
+ [build-system]
25
+ requires = ["hatchling", "hatch-vcs"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.hatch.version]
29
+ source = "vcs"
30
+
31
+ [tool.hatch.version.raw-options]
32
+ fallback_version = "0.0.0"
33
+
34
+ [tool.hatch.build.targets.sdist]
35
+ include = ["src", "tests", "examples", "README.md", "pyproject.toml"]
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/twitchchat"]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8",
43
+ "pytest-asyncio>=1",
44
+ "ruff>=0.13",
45
+ "ty>=0.0.1",
46
+ ]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ asyncio_default_fixture_loop_scope = "function"
51
+ testpaths = ["tests"]
52
+ addopts = ["-ra", "--strict-markers"]
53
+ filterwarnings = ["error"]
54
+
55
+ [tool.ruff]
56
+ line-length = 100
57
+ target-version = "py315"
58
+ src = ["src", "tests", "examples"]
59
+
60
+ [tool.ruff.lint]
61
+ select = [
62
+ "F", "E", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF", "PL", "PT", "RET",
63
+ "ASYNC", "S", "BLE", "FBT", "PIE", "PERF", "TRY", "EM", "G", "LOG", "SLF", "TC",
64
+ "PTH", "D2", "D3", "D4",
65
+ ]
66
+ ignore = [
67
+ "D203", "D213", "D401", "D413",
68
+ "TRY003", # long messages in exception constructors
69
+ "FBT001", "FBT002", # boolean params are part of the public API (tls=, reconnect=)
70
+ ]
71
+
72
+ [tool.ruff.lint.per-file-ignores]
73
+ "tests/**" = ["S101", "S105", "S106", "EM101", "PLR2004", "SLF001", "D", "FBT003"]
74
+ "examples/**" = ["T201", "D"]
75
+
76
+ [tool.ruff.lint.pydocstyle]
77
+ convention = "google"
78
+
79
+ [tool.ruff.format]
80
+ docstring-code-format = true
81
+
82
+ [tool.ty.environment]
83
+ python-version = "3.15"
84
+
85
+ [tool.ty.src]
86
+ include = ["src", "tests", "examples"]
87
+
88
+ [tool.ty.rules]
89
+ possibly-unresolved-reference = "error"