runnotify 0.1.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,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ .env
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-22
10
+
11
+ First release. Extracted from a single-file notifier used to report on
12
+ long-running measurement jobs, and rebuilt around a channel abstraction.
13
+
14
+ ### Added
15
+
16
+ - `Notifier` with `RUNNING`, `PROGRESS`, `COMPLETED`, `CANCELLED`, `HANG`,
17
+ `ERROR` and `OOM` statuses, progress throttling and hang detection.
18
+ - Context manager support: `RUNNING` on entry; `COMPLETED`, `CANCELLED` or
19
+ `ERROR` with a traceback tail on exit, never suppressing the exception.
20
+ - A `Channel` protocol with three ways to reach an implementation — construct
21
+ it, register it by name, or publish it from another distribution through the
22
+ `runnotify.channels` entry-point group.
23
+ - Built-in Slack and Notion channels, each the only module knowing its vendor.
24
+ - Per-channel `min_status` filtering over an explicit severity ordering, so one
25
+ channel can carry alerts and another the full log.
26
+ - Layered configuration: keyword arguments, environment variables, a TOML file
27
+ (`runnotify.toml` or `[tool.runnotify]`), then defaults. A `$VAR` reference to
28
+ an unset variable drops the key rather than emptying it.
29
+ - Bounded retries with exponential backoff, honouring `Retry-After`.
30
+ - An out-of-process OOM watchdog for the `SIGKILL` case `atexit` cannot observe.
31
+ - A `runnotify` command with distinct exit codes and `--dry-run`,
32
+ `--list-channels`, `--channel` and `--version`.
33
+ - Type annotations throughout, with a `py.typed` marker.
34
+
35
+ ### Fixed
36
+
37
+ Relative to the single-file predecessor this package replaces:
38
+
39
+ - Watchdog configuration is passed on stdin rather than interpolated into a
40
+ `python -c` argument, where the webhook URL and Notion API token were readable
41
+ through `ps` by any user on the machine.
42
+ - Terminality is read from the status itself rather than tracked by a flag that
43
+ only the named methods set, which previously let a generic send leave the exit
44
+ hook armed and emit a second, untrue `CANCELLED`.
45
+ - The first progress event is no longer throttled. The previous sentinel of
46
+ `0.0` was compared against `time.monotonic()`, which is uptime-based, so on a
47
+ recently booted host the first event was silently dropped.
48
+ - The Notion log keeps its most recent lines instead of its first ones.
49
+ - `OOM` has a Slack emoji; it was mapped for Notion only and rendered as the
50
+ fallback pin.
51
+ - Delivery failures are logged rather than printed, so a library no longer
52
+ writes to a caller's stdout.
53
+ - `progress` and `oom` are reachable from the command line.
54
+
55
+ [Unreleased]: https://github.com/Saiid2001/runnotify/compare/v0.1.0...HEAD
56
+ [0.1.0]: https://github.com/Saiid2001/runnotify/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saiid El Hajj Chehade
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.5
2
+ Name: runnotify
3
+ Version: 0.1.0
4
+ Summary: Status reporting for long, unattended runs: Slack, Notion, or your own channel.
5
+ Project-URL: Homepage, https://github.com/Saiid2001/runnotify
6
+ Project-URL: Source, https://github.com/Saiid2001/runnotify
7
+ Project-URL: Issues, https://github.com/Saiid2001/runnotify/issues
8
+ Project-URL: Changelog, https://github.com/Saiid2001/runnotify/blob/main/CHANGELOG.md
9
+ Author: Saiid El Hajj Chehade
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: monitoring,notifications,notion,oom,slack,watchdog
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Topic :: Communications :: Chat
24
+ Classifier: Topic :: System :: Monitoring
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Description-Content-Type: text/markdown
28
+
29
+ # runnotify
30
+
31
+ Status reporting for long, unattended runs — to Slack, to Notion, or to a channel
32
+ you write yourself.
33
+
34
+ A job that is meant to be left alone fails in ways nobody is watching for. It
35
+ dies on item 40 of 900. It wedges on something that never returns. The kernel
36
+ kills it and leaves no traceback anywhere. Each of those ends with a short result
37
+ that looks like a result. `runnotify` tells you which one happened.
38
+
39
+ - **No dependencies.** Standard library only, on Python 3.11+.
40
+ - **Optional by construction.** With nothing configured, every call is a no-op
41
+ and your run is unchanged. A notifier must never be the reason a job fails.
42
+ - **Covers the endings you can't catch.** An exit hook reports processes that
43
+ end without saying so; a detached watchdog reports the ones killed with
44
+ `SIGKILL`, which no `atexit` hook will ever see.
45
+ - **Pluggable.** Slack and Notion ship here. A channel is one small class, and a
46
+ separate package can publish one that is discovered on install.
47
+
48
+ ```bash
49
+ pip install runnotify
50
+ ```
51
+
52
+ ## Use
53
+
54
+ ```python
55
+ from runnotify import Notifier
56
+
57
+ with Notifier("nightly-crawl") as run:
58
+ for i, item in enumerate(items):
59
+ process(item)
60
+ run.ping() # "still moving"
61
+ run.progress(f"{i}/{len(items)}", n=i)
62
+ ```
63
+
64
+ The context manager reports `RUNNING` on entry and, on exit, `COMPLETED`,
65
+ `CANCELLED` (on `KeyboardInterrupt` / `SystemExit`) or `ERROR` with the tail of
66
+ the traceback. Exceptions are never suppressed.
67
+
68
+ Without the context manager, call the statuses yourself:
69
+
70
+ ```python
71
+ run = Notifier("nightly-crawl")
72
+ run.start("2,400 pages queued")
73
+ ...
74
+ run.completed("2,381 pages, 19 unreachable")
75
+ run.error("stage 2 failed: connection timeout")
76
+ ```
77
+
78
+ If the process ends with nothing terminal sent, an exit hook reports `CANCELLED`
79
+ rather than leaving the run looking alive forever.
80
+
81
+ ### Surviving the OOM killer
82
+
83
+ `atexit` runs for every ending Python can observe. `SIGKILL` is not one of them,
84
+ and `SIGKILL` is what the Linux OOM killer sends.
85
+
86
+ ```python
87
+ run = Notifier("big-embedding-job")
88
+ run.start_oom_watchdog() # detached child, disarmed by any terminal status
89
+ ```
90
+
91
+ The watchdog polls whether your process still exists and, if it disappears
92
+ without a terminal status, reports `OOM` through your own channels — including
93
+ any you added. POSIX only; on Windows it declines to arm and says so, because
94
+ the liveness probe it uses would terminate the process it is watching.
95
+
96
+ ### Detecting a hang
97
+
98
+ ```python
99
+ run = Notifier("crawl", hang_timeout=1800)
100
+ run.start_hang_watcher() # background thread
101
+ ...
102
+ run.ping() # call wherever real progress happens
103
+ ```
104
+
105
+ Nothing pings for `hang_timeout` seconds and a single `HANG` goes out. `ping()`
106
+ re-arms it, so one stall produces one message.
107
+
108
+ ## Configure
109
+
110
+ Four layers, each overriding the one below: **keyword arguments → environment
111
+ variables → TOML file → defaults.**
112
+
113
+ ```toml
114
+ # runnotify.toml (or [tool.runnotify] in pyproject.toml)
115
+ hang_timeout = 1800
116
+ progress_interval = 300
117
+
118
+ [channels.slack]
119
+ webhook_url = "$NOTIFY_WEBHOOK_URL" # read from the environment
120
+ min_status = "completed" # terminal events only — this is the alert
121
+
122
+ [channels.notion]
123
+ token = "$NOTION_API_TOKEN"
124
+ database_id = "$NOTION_DATABASE_ID"
125
+ min_status = "progress" # everything — this is the log
126
+
127
+ [watchdog]
128
+ oom = true
129
+ ```
130
+
131
+ A value that is exactly `$VAR` or `${VAR}` is read from the environment, and if
132
+ that variable is unset the key is **dropped** rather than set to an empty string
133
+ — a missing secret reads as *not configured*, not as configured with nothing.
134
+
135
+ `min_status` is the knob that makes two channels worth having: severity runs
136
+ `progress < running < completed < cancelled < hang < error < oom`, so
137
+ `min_status = "completed"` selects exactly the terminal statuses.
138
+
139
+ Environment variables work without any file:
140
+
141
+ | Variable | Effect |
142
+ | --- | --- |
143
+ | `NOTIFY_WEBHOOK_URL` | Slack incoming webhook |
144
+ | `NOTION_API_TOKEN`, `NOTION_DATABASE_ID` | Notion database |
145
+ | `NOTIFY_SOURCE` | Machine identifier (default: hostname) |
146
+ | `RUNNOTIFY_<CHANNEL>_<OPTION>` | Any option of any channel, e.g. `RUNNOTIFY_SLACK_MIN_STATUS` |
147
+ | `RUNNOTIFY_TOPIC`, `RUNNOTIFY_DRY_RUN`, `RUNNOTIFY_STRICT`, … | Top-level settings |
148
+
149
+ By default a channel that cannot be configured costs you that channel and
150
+ nothing else — the run starts, the other channels report, and the problem is
151
+ logged and available on `notifier.problems`. Pass `strict=True` when a missing
152
+ webhook should stop the job instead.
153
+
154
+ ## From the shell
155
+
156
+ ```bash
157
+ runnotify --topic nightly-crawl --status completed --message "2,381 pages"
158
+ runnotify --list-channels
159
+ runnotify --topic x --status error --dry-run -v # build it, send nothing
160
+ ```
161
+
162
+ It reads the nearest `.env` the way a run would, and names the file it read when
163
+ nothing is configured — the common case is a webhook sitting in a `.env` that
164
+ the shell never loaded.
165
+
166
+ Exit codes: `0` delivered, `1` a channel failed, `2` nothing was configured.
167
+
168
+ ## Writing a channel
169
+
170
+ A channel consumes events and delivers them. That is the whole contract:
171
+
172
+ ```python
173
+ from runnotify import BaseChannel, Event, Notifier, register
174
+
175
+
176
+ class ConsoleChannel(BaseChannel):
177
+ name = "console"
178
+
179
+ def deliver(self, event: Event) -> None:
180
+ print(f"[{event.status}] {event.topic}: {event.message}")
181
+
182
+
183
+ # 1. pass it in directly
184
+ Notifier("my-run", channels=[ConsoleChannel()])
185
+
186
+ # 2. or register it, and select it by name in runnotify.toml
187
+ register("console", ConsoleChannel)
188
+ ```
189
+
190
+ Raise on failure — the notifier isolates and records it, and a channel that
191
+ swallows its own errors reports success it did not achieve. Subclassing
192
+ `BaseChannel` gets you `min_status` filtering, a `close()` no-op and a `repr`
193
+ that redacts secrets; satisfying the `Channel` protocol directly also works.
194
+
195
+ To publish one from your own package, advertise the entry-point group:
196
+
197
+ ```toml
198
+ [project.entry-points."runnotify.channels"]
199
+ discord = "runnotify_discord:DiscordChannel"
200
+ ```
201
+
202
+ Installing that distribution makes `[channels.discord]` selectable. Nothing in
203
+ `runnotify` changes, and the new channel receives OOM reports from the watchdog
204
+ for free.
205
+
206
+ ## Notion setup
207
+
208
+ Share a database with your integration and give it these properties. Rename any
209
+ of them via `[channels.notion.properties]`; set one to `""` to skip it.
210
+
211
+ | Property | Type | Role |
212
+ | --- | --- | --- |
213
+ | `Name` | Title | The topic |
214
+ | `Status` | Status | `Running` / `Completed` / `Error` / `Hung` / `Cancelled` / `OOM` |
215
+ | `Source` | Text | Machine |
216
+ | `Latest Message` | Text | Most recent message |
217
+ | `Log` | Text | Accumulated history |
218
+ | `Started At`, `Last Updated` | Date | |
219
+ | `Progress` | Number | |
220
+
221
+ A `PROGRESS` event deliberately leaves `Status` alone, so a progress ping does
222
+ not flicker the row out of `Running`.
223
+
224
+ ## License
225
+
226
+ MIT
@@ -0,0 +1,198 @@
1
+ # runnotify
2
+
3
+ Status reporting for long, unattended runs — to Slack, to Notion, or to a channel
4
+ you write yourself.
5
+
6
+ A job that is meant to be left alone fails in ways nobody is watching for. It
7
+ dies on item 40 of 900. It wedges on something that never returns. The kernel
8
+ kills it and leaves no traceback anywhere. Each of those ends with a short result
9
+ that looks like a result. `runnotify` tells you which one happened.
10
+
11
+ - **No dependencies.** Standard library only, on Python 3.11+.
12
+ - **Optional by construction.** With nothing configured, every call is a no-op
13
+ and your run is unchanged. A notifier must never be the reason a job fails.
14
+ - **Covers the endings you can't catch.** An exit hook reports processes that
15
+ end without saying so; a detached watchdog reports the ones killed with
16
+ `SIGKILL`, which no `atexit` hook will ever see.
17
+ - **Pluggable.** Slack and Notion ship here. A channel is one small class, and a
18
+ separate package can publish one that is discovered on install.
19
+
20
+ ```bash
21
+ pip install runnotify
22
+ ```
23
+
24
+ ## Use
25
+
26
+ ```python
27
+ from runnotify import Notifier
28
+
29
+ with Notifier("nightly-crawl") as run:
30
+ for i, item in enumerate(items):
31
+ process(item)
32
+ run.ping() # "still moving"
33
+ run.progress(f"{i}/{len(items)}", n=i)
34
+ ```
35
+
36
+ The context manager reports `RUNNING` on entry and, on exit, `COMPLETED`,
37
+ `CANCELLED` (on `KeyboardInterrupt` / `SystemExit`) or `ERROR` with the tail of
38
+ the traceback. Exceptions are never suppressed.
39
+
40
+ Without the context manager, call the statuses yourself:
41
+
42
+ ```python
43
+ run = Notifier("nightly-crawl")
44
+ run.start("2,400 pages queued")
45
+ ...
46
+ run.completed("2,381 pages, 19 unreachable")
47
+ run.error("stage 2 failed: connection timeout")
48
+ ```
49
+
50
+ If the process ends with nothing terminal sent, an exit hook reports `CANCELLED`
51
+ rather than leaving the run looking alive forever.
52
+
53
+ ### Surviving the OOM killer
54
+
55
+ `atexit` runs for every ending Python can observe. `SIGKILL` is not one of them,
56
+ and `SIGKILL` is what the Linux OOM killer sends.
57
+
58
+ ```python
59
+ run = Notifier("big-embedding-job")
60
+ run.start_oom_watchdog() # detached child, disarmed by any terminal status
61
+ ```
62
+
63
+ The watchdog polls whether your process still exists and, if it disappears
64
+ without a terminal status, reports `OOM` through your own channels — including
65
+ any you added. POSIX only; on Windows it declines to arm and says so, because
66
+ the liveness probe it uses would terminate the process it is watching.
67
+
68
+ ### Detecting a hang
69
+
70
+ ```python
71
+ run = Notifier("crawl", hang_timeout=1800)
72
+ run.start_hang_watcher() # background thread
73
+ ...
74
+ run.ping() # call wherever real progress happens
75
+ ```
76
+
77
+ Nothing pings for `hang_timeout` seconds and a single `HANG` goes out. `ping()`
78
+ re-arms it, so one stall produces one message.
79
+
80
+ ## Configure
81
+
82
+ Four layers, each overriding the one below: **keyword arguments → environment
83
+ variables → TOML file → defaults.**
84
+
85
+ ```toml
86
+ # runnotify.toml (or [tool.runnotify] in pyproject.toml)
87
+ hang_timeout = 1800
88
+ progress_interval = 300
89
+
90
+ [channels.slack]
91
+ webhook_url = "$NOTIFY_WEBHOOK_URL" # read from the environment
92
+ min_status = "completed" # terminal events only — this is the alert
93
+
94
+ [channels.notion]
95
+ token = "$NOTION_API_TOKEN"
96
+ database_id = "$NOTION_DATABASE_ID"
97
+ min_status = "progress" # everything — this is the log
98
+
99
+ [watchdog]
100
+ oom = true
101
+ ```
102
+
103
+ A value that is exactly `$VAR` or `${VAR}` is read from the environment, and if
104
+ that variable is unset the key is **dropped** rather than set to an empty string
105
+ — a missing secret reads as *not configured*, not as configured with nothing.
106
+
107
+ `min_status` is the knob that makes two channels worth having: severity runs
108
+ `progress < running < completed < cancelled < hang < error < oom`, so
109
+ `min_status = "completed"` selects exactly the terminal statuses.
110
+
111
+ Environment variables work without any file:
112
+
113
+ | Variable | Effect |
114
+ | --- | --- |
115
+ | `NOTIFY_WEBHOOK_URL` | Slack incoming webhook |
116
+ | `NOTION_API_TOKEN`, `NOTION_DATABASE_ID` | Notion database |
117
+ | `NOTIFY_SOURCE` | Machine identifier (default: hostname) |
118
+ | `RUNNOTIFY_<CHANNEL>_<OPTION>` | Any option of any channel, e.g. `RUNNOTIFY_SLACK_MIN_STATUS` |
119
+ | `RUNNOTIFY_TOPIC`, `RUNNOTIFY_DRY_RUN`, `RUNNOTIFY_STRICT`, … | Top-level settings |
120
+
121
+ By default a channel that cannot be configured costs you that channel and
122
+ nothing else — the run starts, the other channels report, and the problem is
123
+ logged and available on `notifier.problems`. Pass `strict=True` when a missing
124
+ webhook should stop the job instead.
125
+
126
+ ## From the shell
127
+
128
+ ```bash
129
+ runnotify --topic nightly-crawl --status completed --message "2,381 pages"
130
+ runnotify --list-channels
131
+ runnotify --topic x --status error --dry-run -v # build it, send nothing
132
+ ```
133
+
134
+ It reads the nearest `.env` the way a run would, and names the file it read when
135
+ nothing is configured — the common case is a webhook sitting in a `.env` that
136
+ the shell never loaded.
137
+
138
+ Exit codes: `0` delivered, `1` a channel failed, `2` nothing was configured.
139
+
140
+ ## Writing a channel
141
+
142
+ A channel consumes events and delivers them. That is the whole contract:
143
+
144
+ ```python
145
+ from runnotify import BaseChannel, Event, Notifier, register
146
+
147
+
148
+ class ConsoleChannel(BaseChannel):
149
+ name = "console"
150
+
151
+ def deliver(self, event: Event) -> None:
152
+ print(f"[{event.status}] {event.topic}: {event.message}")
153
+
154
+
155
+ # 1. pass it in directly
156
+ Notifier("my-run", channels=[ConsoleChannel()])
157
+
158
+ # 2. or register it, and select it by name in runnotify.toml
159
+ register("console", ConsoleChannel)
160
+ ```
161
+
162
+ Raise on failure — the notifier isolates and records it, and a channel that
163
+ swallows its own errors reports success it did not achieve. Subclassing
164
+ `BaseChannel` gets you `min_status` filtering, a `close()` no-op and a `repr`
165
+ that redacts secrets; satisfying the `Channel` protocol directly also works.
166
+
167
+ To publish one from your own package, advertise the entry-point group:
168
+
169
+ ```toml
170
+ [project.entry-points."runnotify.channels"]
171
+ discord = "runnotify_discord:DiscordChannel"
172
+ ```
173
+
174
+ Installing that distribution makes `[channels.discord]` selectable. Nothing in
175
+ `runnotify` changes, and the new channel receives OOM reports from the watchdog
176
+ for free.
177
+
178
+ ## Notion setup
179
+
180
+ Share a database with your integration and give it these properties. Rename any
181
+ of them via `[channels.notion.properties]`; set one to `""` to skip it.
182
+
183
+ | Property | Type | Role |
184
+ | --- | --- | --- |
185
+ | `Name` | Title | The topic |
186
+ | `Status` | Status | `Running` / `Completed` / `Error` / `Hung` / `Cancelled` / `OOM` |
187
+ | `Source` | Text | Machine |
188
+ | `Latest Message` | Text | Most recent message |
189
+ | `Log` | Text | Accumulated history |
190
+ | `Started At`, `Last Updated` | Date | |
191
+ | `Progress` | Number | |
192
+
193
+ A `PROGRESS` event deliberately leaves `Status` alone, so a progress ping does
194
+ not flicker the row out of `Running`.
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,109 @@
1
+ [project]
2
+ name = "runnotify"
3
+ dynamic = ["version"]
4
+ description = "Status reporting for long, unattended runs: Slack, Notion, or your own channel."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ authors = [{ name = "Saiid El Hajj Chehade" }]
10
+ keywords = ["notifications", "slack", "notion", "monitoring", "watchdog", "oom"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Science/Research",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Programming Language :: Python :: Implementation :: CPython",
22
+ "Topic :: System :: Monitoring",
23
+ "Topic :: Communications :: Chat",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ # No runtime dependencies, deliberately. This package is imported by every run in
28
+ # a study; a dependency here is a dependency there, and a resolver conflict here
29
+ # would be a resolver conflict in work that has nothing to do with notifications.
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/Saiid2001/runnotify"
34
+ Source = "https://github.com/Saiid2001/runnotify"
35
+ Issues = "https://github.com/Saiid2001/runnotify/issues"
36
+ Changelog = "https://github.com/Saiid2001/runnotify/blob/main/CHANGELOG.md"
37
+
38
+ [project.scripts]
39
+ runnotify = "runnotify._cli:main"
40
+
41
+ # Channels published by other distributions are discovered through this group.
42
+ # The built-ins are registered directly in runnotify.channels; the group is here
43
+ # so `runnotify --list-channels` documents the extension point by example.
44
+ [project.entry-points."runnotify.channels"]
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=8",
49
+ "pytest-cov>=5",
50
+ "mypy>=1.11",
51
+ "ruff>=0.6",
52
+ ]
53
+
54
+ [build-system]
55
+ requires = ["hatchling"]
56
+ build-backend = "hatchling.build"
57
+
58
+ [tool.hatch.version]
59
+ path = "src/runnotify/__init__.py"
60
+
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["src/runnotify"]
63
+
64
+ [tool.hatch.build.targets.sdist]
65
+ include = ["/src", "/tests", "/README.md", "/LICENSE", "/CHANGELOG.md"]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
69
+ addopts = "-q --strict-markers --strict-config"
70
+ filterwarnings = ["error"]
71
+
72
+ [tool.coverage.run]
73
+ source = ["runnotify"]
74
+ branch = true
75
+
76
+ [tool.coverage.report]
77
+ exclude_lines = [
78
+ "pragma: no cover",
79
+ "raise NotImplementedError",
80
+ "if __name__ == .__main__.:",
81
+ "if TYPE_CHECKING:",
82
+ ]
83
+
84
+ [tool.ruff]
85
+ line-length = 100
86
+ target-version = "py311"
87
+ src = ["src", "tests"]
88
+
89
+ [tool.ruff.lint]
90
+ select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM", "RUF"]
91
+ ignore = [
92
+ # The notifier catches broad exceptions on purpose: a channel failure must
93
+ # never propagate into the run being reported on.
94
+ "BLE001",
95
+ ]
96
+
97
+ [tool.ruff.lint.per-file-ignores]
98
+ "tests/*" = ["S101"]
99
+
100
+ [tool.mypy]
101
+ python_version = "3.11"
102
+ files = ["src/runnotify"]
103
+ strict = true
104
+ warn_unreachable = true
105
+ enable_error_code = ["ignore-without-code", "redundant-expr"]
106
+
107
+ [[tool.mypy.overrides]]
108
+ module = "tests.*"
109
+ strict = false