shop-notify 1.0.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.
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: shop-notify
3
+ Version: 1.0.1
4
+ Summary: Watch an Etsy shop for new listings and send notifications.
5
+ Keywords: etsy,notifications,apprise,shop,new-listings,telegram,ntfy
6
+ Author: Reik Keutterling
7
+ Author-email: Reik Keutterling <spielkind@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Environment :: No Input/Output (Daemon)
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
+ Classifier: Topic :: Communications
20
+ Classifier: Topic :: Office/Business
21
+ Requires-Dist: apprise>=2.0.1
22
+ Requires-Dist: httpx>=0.28
23
+ Requires-Dist: jinja2>=3.1.6
24
+ Requires-Dist: pydantic>=2.12
25
+ Requires-Dist: pydantic-settings>=2.11
26
+ Requires-Python: >=3.14
27
+ Project-URL: Homepage, https://github.com/spielkind/shop-notify
28
+ Project-URL: Documentation, https://github.com/spielkind/shop-notify#readme
29
+ Project-URL: Changelog, https://github.com/spielkind/shop-notify/blob/main/CHANGELOG.md
30
+ Project-URL: Issues, https://github.com/spielkind/shop-notify/issues
31
+ Project-URL: Source, https://github.com/spielkind/shop-notify
32
+ Description-Content-Type: text/markdown
33
+
34
+ <picture>
35
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/banner-dark.svg">
36
+ <img alt="Shop Notify – get notified when new items appear in a shop" src="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/banner-light.svg">
37
+ </picture>
38
+
39
+ # Shop Notify
40
+
41
+ [![CI](https://github.com/spielkind/shop-notify/actions/workflows/ci.yml/badge.svg)](https://github.com/spielkind/shop-notify/actions/workflows/ci.yml)
42
+ [![Docker](https://github.com/spielkind/shop-notify/actions/workflows/docker.yml/badge.svg)](https://github.com/spielkind/shop-notify/actions/workflows/docker.yml)
43
+ [![Coverage](https://raw.githubusercontent.com/spielkind/shop-notify/python-coverage-comment-action-data/badge.svg)](https://github.com/spielkind/shop-notify/tree/python-coverage-comment-action-data)
44
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/spielkind/shop-notify/badge)](https://scorecard.dev/viewer/?uri=github.com/spielkind/shop-notify)
45
+ [![PyPI](https://img.shields.io/pypi/v/shop-notify)](https://pypi.org/project/shop-notify/)
46
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15766/badge)](https://www.bestpractices.dev/projects/15766)
47
+
48
+ Shop Notify watches an Etsy shop and tells you – and anyone else you like – as soon as new items appear. Perfect for one-of-a-kind pieces that sell out quickly.
49
+
50
+ - **New and re-appeared items:** every listing is reported once; if it disappears and comes back later, you hear about it again.
51
+ - **Any notification channel:** HTML e-mail with item images to as many recipients as you want, push to your phone via ntfy, Telegram, Signal and [100+ more](https://github.com/caronc/apprise/wiki) – also several at once.
52
+ - **Runs unattended:** made for a Raspberry Pi, as a Docker container or systemd service, with health check, watchdog and Etsy rate-limit handling.
53
+
54
+ ## How it works
55
+
56
+ <picture>
57
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/how-it-works-dark.svg">
58
+ <img alt="Shop Notify polls the Etsy Open API, compares the active listings with its SQLite database and sends new items via Apprise to e-mail, push and chat services." src="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/how-it-works-light.svg">
59
+ </picture>
60
+
61
+ Every few minutes Shop Notify fetches the active listings of the shop from the [Etsy Open API v3](https://developers.etsy.com/documentation/) and compares them with the items it has already seen. Each new item is notified exactly once:
62
+
63
+ <picture>
64
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/lifecycle-dark.svg">
65
+ <img alt="Lifecycle of a listing: New, then Notified once sent, then Forgotten when it disappears from the shop; if it re-appears it is New again. Failed notifications are retried with the next poll." src="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/lifecycle-light.svg">
66
+ </picture>
67
+
68
+ ### First run
69
+ When the database is empty, all items currently in the shop would be "new". Instead of flooding your phone, Shop Notify only **records** them on the first run and prints them to the console; notifications start with the next new item:
70
+
71
+ ```text
72
+ First run: 87 items currently in YourShop (recorded, not notified)
73
+ …
74
+ ```
75
+
76
+ Set `NOTIFY_EXISTING_ON_FIRST_RUN=true` to send them to all targets anyway. To start over, delete the database (`DB_PATH`).
77
+
78
+ ### Example notification
79
+
80
+ A notification on your phone (plain text) looks like this – e-mails additionally show the item picture:
81
+
82
+ ```text
83
+ 2 new items in YourShop
84
+
85
+ Hand-thrown mug "Morning dew"
86
+ €24.50 · Oct 14, 2026, 19:46
87
+ https://www.etsy.com/listing/1234567890
88
+
89
+ Small vase in sage green
90
+ €18.00 · Oct 14, 2026, 20:46
91
+ https://www.etsy.com/listing/1234567891
92
+ ```
93
+
94
+ ## Quick start
95
+
96
+ 1. **Get an Etsy API key** – see [Creating an Etsy app](https://github.com/spielkind/shop-notify/blob/main/docs/etsy-app.md). You need the *keystring* and the *shared secret*.
97
+ 2. **Configure:** download [`compose.yaml`](https://github.com/spielkind/shop-notify/blob/main/compose.yaml) and [`.env.example`](https://github.com/spielkind/shop-notify/blob/main/.env.example), save the latter as `.env` and fill in your key, secret and the shop name (as in `etsy.com/shop/<name>`).
98
+ 3. **Start:**
99
+
100
+ ```sh
101
+ mkdir -p data
102
+ docker compose up -d
103
+ docker compose logs -f
104
+ ```
105
+
106
+ Without notification targets, new items are only written to the log – a good first test. Then [set up notifications](https://github.com/spielkind/shop-notify/blob/main/docs/notifications.md).
107
+
108
+ ## Configuration
109
+
110
+ All settings are read from `.env` in the working directory or from environment variables (which take precedence).
111
+
112
+ | Variable | Default | Description |
113
+ |---|---|---|
114
+ | `ETSY_API_KEY` | *required* | Keystring of your Etsy app |
115
+ | `ETSY_SHARED_SECRET` | *required* | Shared secret of your Etsy app |
116
+ | `ETSY_SHOP_NAME` | – | Shop to watch, as in `etsy.com/shop/<name>`; the ID is looked up at startup |
117
+ | `ETSY_SHOP_ID` | – | Numeric shop ID, alternative to `ETSY_SHOP_NAME` |
118
+ | `POLL_INTERVAL` | `300` | Seconds between two polls (minimum 10), see [below](#choosing-the-poll-interval) |
119
+ | `NOTIFY_CONFIG` | – | Path to an Apprise YAML file with notification targets |
120
+ | `NOTIFY_URLS` | – | One or more Apprise URLs, short form for simple setups |
121
+ | `NOTIFY_CONSOLE` | `auto` | Print notifications to stdout: `auto` only if no other targets are configured (plus the [first run](#first-run)), `true` always (in addition to them), `false` never |
122
+ | `NOTIFY_EXISTING_ON_FIRST_RUN` | `false` | Send the items already in the shop on the [first run](#first-run) to all targets |
123
+ | `NOTIFY_LANGUAGE` | `en` | Language of the built-in messages: `en` or `de` |
124
+ | `NOTIFY_IMAGES` | `true` | Push/chat services get one message per new item with its picture |
125
+ | `NOTIFY_IMAGES_MAX` | `5` | Up to this many new items per poll are sent individually; above, one summary message |
126
+ | `NOTIFY_IMAGES_IN_SUMMARY` | `false` | Attach up to 10 pictures to the summary message (a Telegram album with `?album=yes`) |
127
+ | `NOTIFY_TEMPLATE_DIR` | – | Directory with [custom message templates](https://github.com/spielkind/shop-notify/blob/main/docs/notifications.md#message-templates) |
128
+ | `DB_PATH` | `shop-notify.db` | SQLite database of seen items |
129
+ | `HEARTBEAT_FILE` | next to `DB_PATH` | Touched after every successful poll, used by the [health check](#health-check) |
130
+ | `LOG_FILE` | `shop-notify.log` | Log file; empty to log to stdout only |
131
+ | `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, … |
132
+ | `ETSY_API_ENDPOINT` | Etsy Open API v3 | Only needed for testing |
133
+ | `TZ` | UTC | Docker only: time zone for dates in notifications and logs, e.g. `Europe/Berlin` (locally and with systemd the system time zone is used) |
134
+
135
+ Only listing ID, title, URL, price and modification time are stored locally; vanished listings are deleted once they were notified.
136
+
137
+ ### Choosing the poll interval
138
+ A new Etsy app may make **5,000 requests per day**. Every poll needs one request per 100 active listings, so stay above *35 s × pages* to use at most half of the quota:
139
+
140
+ | Active listings | Requests per poll | Recommended `POLL_INTERVAL` |
141
+ |---|---|---|
142
+ | up to 100 | 1 | ≥ 35 s |
143
+ | up to 200 | 2 | ≥ 70 s |
144
+ | up to 500 | 5 | ≥ 3 min |
145
+
146
+ The default of 5 minutes needs fewer than 300 requests per day for a typical shop. Shop Notify logs a warning when less than 20 % of the daily quota is left, and waits as long as Etsy asks when it answers `429 Too Many Requests`.
147
+
148
+ ### Notifications
149
+ Notifications are sent with [Apprise](https://github.com/caronc/apprise) – e-mail, ntfy, Telegram, Signal and [100+ more](https://github.com/caronc/apprise/wiki). Copy [`notify.example.yml`](https://github.com/spielkind/shop-notify/blob/main/notify.example.yml) to `notify.yml`, adjust it and set `NOTIFY_CONFIG=notify.yml`:
150
+
151
+ ```yaml
152
+ version: 1
153
+ urls:
154
+ # One e-mail to several people who don't see each other's address
155
+ - mailtos://you:app-password@gmail.com:
156
+ - bcc: alice@example.com, bob@example.com
157
+ # Push notification to your phone
158
+ - ntfys://ntfy.sh/your-long-random-topic
159
+ ```
160
+
161
+ E-mail gets one HTML message with all new items; push and chat services get one message per item with its picture. Try your setup with `shop-notify test-notify` – no Etsy key needed.
162
+
163
+ **→ [Notifications in detail](https://github.com/spielkind/shop-notify/blob/main/docs/notifications.md):** setting up Gmail, ntfy and Telegram, pictures and summaries, testing, custom templates and languages.
164
+
165
+ ## Creating an Etsy app
166
+ Shop Notify only reads public listings, so it needs an API key (keystring and shared secret) but no OAuth login. New apps start as *Pending Personal Approval*; until they are **Active**, every request fails with `403`. **→ [Step-by-step registration](https://github.com/spielkind/shop-notify/blob/main/docs/etsy-app.md)**
167
+
168
+ ## Running
169
+
170
+ ### Docker
171
+ Images for `linux/amd64` and `linux/arm64` (Raspberry Pi with a 64-bit OS) are published as `ghcr.io/spielkind/shop-notify` (`latest` = newest release). With [`compose.yaml`](https://github.com/spielkind/shop-notify/blob/main/compose.yaml) and your `.env`:
172
+
173
+ ```sh
174
+ mkdir -p data
175
+ docker compose pull
176
+ docker compose up -d
177
+ docker compose logs -f
178
+ ```
179
+
180
+ To use `notify.yml`, uncomment its volume in `compose.yaml` and set `NOTIFY_CONFIG=/config/notify.yml`; set `TZ=Europe/Berlin` (or your time zone) as the container runs in UTC otherwise. **→ [Docker in detail](https://github.com/spielkind/shop-notify/blob/main/docs/docker.md):** image tags, updates, moving an existing database, verifying the image, troubleshooting.
181
+
182
+ ### systemd
183
+ Alternatively install it as a sandboxed systemd service, e.g. on Raspberry Pi OS; `systemctl status` then shows the result of the last poll. **→ [systemd setup](https://github.com/spielkind/shop-notify/blob/main/docs/systemd.md)**
184
+
185
+ ### Locally
186
+ Install it from [PyPI](https://pypi.org/project/shop-notify/) with [uv](https://docs.astral.sh/uv/) (it downloads Python 3.14+ if needed) or [pipx](https://pipx.pypa.io/):
187
+
188
+ ```sh
189
+ uv tool install shop-notify # or: pipx install shop-notify
190
+ # create .env in the current directory (see .env.example) and fill it in
191
+ shop-notify
192
+ ```
193
+
194
+ Update with `uv tool upgrade shop-notify`. To run it from a clone of the repository instead: `uv sync`, then `uv run shop-notify`.
195
+
196
+ Stop it with `Ctrl+C`.
197
+
198
+ ### Health check
199
+ After every successful poll Shop Notify touches a heartbeat file. `shop-notify healthcheck` exits with `0` if the last successful poll is younger than `2 × POLL_INTERVAL + 60 s`, otherwise with `1` – e.g. when Etsy rejects the API key or the network is down. Docker uses it for its health status; manually:
200
+
201
+ ```sh
202
+ shop-notify healthcheck # local
203
+ docker compose exec shop-notify shop-notify healthcheck # Docker
204
+ ```
205
+
206
+ ## FAQ
207
+
208
+ <details>
209
+ <summary>Why not simply use an RSS feed?</summary>
210
+
211
+ Etsy doesn't provide an official, reliable feed of new shop listings. The Open API is the supported way to read them.
212
+ </details>
213
+
214
+ <details>
215
+ <summary>Every request fails with <code>403 API key not found or not active</code></summary>
216
+
217
+ Your Etsy app is still *Pending Personal Approval*, or keystring and shared secret are mixed up. Check the status under <https://www.etsy.com/developers/your-apps>.
218
+ </details>
219
+
220
+ <details>
221
+ <summary>I received the same item twice</summary>
222
+
223
+ That's intended if the item was gone from the shop in between (sold, deactivated, renewed) – re-appeared items are reported again. It also happens if all notification targets failed: then the items are retried with the next poll.
224
+ </details>
225
+
226
+ <details>
227
+ <summary>Can I watch several shops?</summary>
228
+
229
+ Not yet with a single instance ([planned](https://github.com/spielkind/shop-notify/issues/7)) – run one instance per shop with its own `.env` and `DB_PATH`. Keep the shared API quota in mind.
230
+ </details>
231
+
232
+ <details>
233
+ <summary>Why didn't I get notified about the items already in the shop?</summary>
234
+
235
+ On the [first run](#first-run) existing items are only recorded, so you are not flooded with notifications. Set `NOTIFY_EXISTING_ON_FIRST_RUN=true` and delete the database to receive them once.
236
+ </details>
237
+
238
+ ## Development
239
+
240
+ ```sh
241
+ uv sync
242
+ uv run pre-commit install
243
+ uv run pytest
244
+ ```
245
+
246
+ See [CONTRIBUTING.md](https://github.com/spielkind/shop-notify/blob/main/CONTRIBUTING.md) for the development workflow, pull requests and releases. Planned features are on the [roadmap](https://github.com/spielkind/shop-notify/issues?q=is%3Aopen+label%3Aroadmap); security issues please report as described in [SECURITY.md](https://github.com/spielkind/shop-notify/blob/main/SECURITY.md).
247
+
248
+ - [Architecture](https://github.com/spielkind/shop-notify/blob/main/docs/architecture.md) and [security model](https://github.com/spielkind/shop-notify/blob/main/docs/security.md)
249
+ - [Governance](https://github.com/spielkind/shop-notify/blob/main/GOVERNANCE.md) and [Code of Conduct](https://github.com/spielkind/shop-notify/blob/main/CODE_OF_CONDUCT.md)
250
+
251
+ ## License and disclaimer
252
+ [MIT](https://github.com/spielkind/shop-notify/blob/main/LICENSE).
253
+
254
+ The term "Etsy" is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
@@ -0,0 +1,221 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/banner-dark.svg">
3
+ <img alt="Shop Notify – get notified when new items appear in a shop" src="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/banner-light.svg">
4
+ </picture>
5
+
6
+ # Shop Notify
7
+
8
+ [![CI](https://github.com/spielkind/shop-notify/actions/workflows/ci.yml/badge.svg)](https://github.com/spielkind/shop-notify/actions/workflows/ci.yml)
9
+ [![Docker](https://github.com/spielkind/shop-notify/actions/workflows/docker.yml/badge.svg)](https://github.com/spielkind/shop-notify/actions/workflows/docker.yml)
10
+ [![Coverage](https://raw.githubusercontent.com/spielkind/shop-notify/python-coverage-comment-action-data/badge.svg)](https://github.com/spielkind/shop-notify/tree/python-coverage-comment-action-data)
11
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/spielkind/shop-notify/badge)](https://scorecard.dev/viewer/?uri=github.com/spielkind/shop-notify)
12
+ [![PyPI](https://img.shields.io/pypi/v/shop-notify)](https://pypi.org/project/shop-notify/)
13
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15766/badge)](https://www.bestpractices.dev/projects/15766)
14
+
15
+ Shop Notify watches an Etsy shop and tells you – and anyone else you like – as soon as new items appear. Perfect for one-of-a-kind pieces that sell out quickly.
16
+
17
+ - **New and re-appeared items:** every listing is reported once; if it disappears and comes back later, you hear about it again.
18
+ - **Any notification channel:** HTML e-mail with item images to as many recipients as you want, push to your phone via ntfy, Telegram, Signal and [100+ more](https://github.com/caronc/apprise/wiki) – also several at once.
19
+ - **Runs unattended:** made for a Raspberry Pi, as a Docker container or systemd service, with health check, watchdog and Etsy rate-limit handling.
20
+
21
+ ## How it works
22
+
23
+ <picture>
24
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/how-it-works-dark.svg">
25
+ <img alt="Shop Notify polls the Etsy Open API, compares the active listings with its SQLite database and sends new items via Apprise to e-mail, push and chat services." src="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/how-it-works-light.svg">
26
+ </picture>
27
+
28
+ Every few minutes Shop Notify fetches the active listings of the shop from the [Etsy Open API v3](https://developers.etsy.com/documentation/) and compares them with the items it has already seen. Each new item is notified exactly once:
29
+
30
+ <picture>
31
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/lifecycle-dark.svg">
32
+ <img alt="Lifecycle of a listing: New, then Notified once sent, then Forgotten when it disappears from the shop; if it re-appears it is New again. Failed notifications are retried with the next poll." src="https://raw.githubusercontent.com/spielkind/shop-notify/main/docs/lifecycle-light.svg">
33
+ </picture>
34
+
35
+ ### First run
36
+ When the database is empty, all items currently in the shop would be "new". Instead of flooding your phone, Shop Notify only **records** them on the first run and prints them to the console; notifications start with the next new item:
37
+
38
+ ```text
39
+ First run: 87 items currently in YourShop (recorded, not notified)
40
+ …
41
+ ```
42
+
43
+ Set `NOTIFY_EXISTING_ON_FIRST_RUN=true` to send them to all targets anyway. To start over, delete the database (`DB_PATH`).
44
+
45
+ ### Example notification
46
+
47
+ A notification on your phone (plain text) looks like this – e-mails additionally show the item picture:
48
+
49
+ ```text
50
+ 2 new items in YourShop
51
+
52
+ Hand-thrown mug "Morning dew"
53
+ €24.50 · Oct 14, 2026, 19:46
54
+ https://www.etsy.com/listing/1234567890
55
+
56
+ Small vase in sage green
57
+ €18.00 · Oct 14, 2026, 20:46
58
+ https://www.etsy.com/listing/1234567891
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ 1. **Get an Etsy API key** – see [Creating an Etsy app](https://github.com/spielkind/shop-notify/blob/main/docs/etsy-app.md). You need the *keystring* and the *shared secret*.
64
+ 2. **Configure:** download [`compose.yaml`](https://github.com/spielkind/shop-notify/blob/main/compose.yaml) and [`.env.example`](https://github.com/spielkind/shop-notify/blob/main/.env.example), save the latter as `.env` and fill in your key, secret and the shop name (as in `etsy.com/shop/<name>`).
65
+ 3. **Start:**
66
+
67
+ ```sh
68
+ mkdir -p data
69
+ docker compose up -d
70
+ docker compose logs -f
71
+ ```
72
+
73
+ Without notification targets, new items are only written to the log – a good first test. Then [set up notifications](https://github.com/spielkind/shop-notify/blob/main/docs/notifications.md).
74
+
75
+ ## Configuration
76
+
77
+ All settings are read from `.env` in the working directory or from environment variables (which take precedence).
78
+
79
+ | Variable | Default | Description |
80
+ |---|---|---|
81
+ | `ETSY_API_KEY` | *required* | Keystring of your Etsy app |
82
+ | `ETSY_SHARED_SECRET` | *required* | Shared secret of your Etsy app |
83
+ | `ETSY_SHOP_NAME` | – | Shop to watch, as in `etsy.com/shop/<name>`; the ID is looked up at startup |
84
+ | `ETSY_SHOP_ID` | – | Numeric shop ID, alternative to `ETSY_SHOP_NAME` |
85
+ | `POLL_INTERVAL` | `300` | Seconds between two polls (minimum 10), see [below](#choosing-the-poll-interval) |
86
+ | `NOTIFY_CONFIG` | – | Path to an Apprise YAML file with notification targets |
87
+ | `NOTIFY_URLS` | – | One or more Apprise URLs, short form for simple setups |
88
+ | `NOTIFY_CONSOLE` | `auto` | Print notifications to stdout: `auto` only if no other targets are configured (plus the [first run](#first-run)), `true` always (in addition to them), `false` never |
89
+ | `NOTIFY_EXISTING_ON_FIRST_RUN` | `false` | Send the items already in the shop on the [first run](#first-run) to all targets |
90
+ | `NOTIFY_LANGUAGE` | `en` | Language of the built-in messages: `en` or `de` |
91
+ | `NOTIFY_IMAGES` | `true` | Push/chat services get one message per new item with its picture |
92
+ | `NOTIFY_IMAGES_MAX` | `5` | Up to this many new items per poll are sent individually; above, one summary message |
93
+ | `NOTIFY_IMAGES_IN_SUMMARY` | `false` | Attach up to 10 pictures to the summary message (a Telegram album with `?album=yes`) |
94
+ | `NOTIFY_TEMPLATE_DIR` | – | Directory with [custom message templates](https://github.com/spielkind/shop-notify/blob/main/docs/notifications.md#message-templates) |
95
+ | `DB_PATH` | `shop-notify.db` | SQLite database of seen items |
96
+ | `HEARTBEAT_FILE` | next to `DB_PATH` | Touched after every successful poll, used by the [health check](#health-check) |
97
+ | `LOG_FILE` | `shop-notify.log` | Log file; empty to log to stdout only |
98
+ | `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, … |
99
+ | `ETSY_API_ENDPOINT` | Etsy Open API v3 | Only needed for testing |
100
+ | `TZ` | UTC | Docker only: time zone for dates in notifications and logs, e.g. `Europe/Berlin` (locally and with systemd the system time zone is used) |
101
+
102
+ Only listing ID, title, URL, price and modification time are stored locally; vanished listings are deleted once they were notified.
103
+
104
+ ### Choosing the poll interval
105
+ A new Etsy app may make **5,000 requests per day**. Every poll needs one request per 100 active listings, so stay above *35 s × pages* to use at most half of the quota:
106
+
107
+ | Active listings | Requests per poll | Recommended `POLL_INTERVAL` |
108
+ |---|---|---|
109
+ | up to 100 | 1 | ≥ 35 s |
110
+ | up to 200 | 2 | ≥ 70 s |
111
+ | up to 500 | 5 | ≥ 3 min |
112
+
113
+ The default of 5 minutes needs fewer than 300 requests per day for a typical shop. Shop Notify logs a warning when less than 20 % of the daily quota is left, and waits as long as Etsy asks when it answers `429 Too Many Requests`.
114
+
115
+ ### Notifications
116
+ Notifications are sent with [Apprise](https://github.com/caronc/apprise) – e-mail, ntfy, Telegram, Signal and [100+ more](https://github.com/caronc/apprise/wiki). Copy [`notify.example.yml`](https://github.com/spielkind/shop-notify/blob/main/notify.example.yml) to `notify.yml`, adjust it and set `NOTIFY_CONFIG=notify.yml`:
117
+
118
+ ```yaml
119
+ version: 1
120
+ urls:
121
+ # One e-mail to several people who don't see each other's address
122
+ - mailtos://you:app-password@gmail.com:
123
+ - bcc: alice@example.com, bob@example.com
124
+ # Push notification to your phone
125
+ - ntfys://ntfy.sh/your-long-random-topic
126
+ ```
127
+
128
+ E-mail gets one HTML message with all new items; push and chat services get one message per item with its picture. Try your setup with `shop-notify test-notify` – no Etsy key needed.
129
+
130
+ **→ [Notifications in detail](https://github.com/spielkind/shop-notify/blob/main/docs/notifications.md):** setting up Gmail, ntfy and Telegram, pictures and summaries, testing, custom templates and languages.
131
+
132
+ ## Creating an Etsy app
133
+ Shop Notify only reads public listings, so it needs an API key (keystring and shared secret) but no OAuth login. New apps start as *Pending Personal Approval*; until they are **Active**, every request fails with `403`. **→ [Step-by-step registration](https://github.com/spielkind/shop-notify/blob/main/docs/etsy-app.md)**
134
+
135
+ ## Running
136
+
137
+ ### Docker
138
+ Images for `linux/amd64` and `linux/arm64` (Raspberry Pi with a 64-bit OS) are published as `ghcr.io/spielkind/shop-notify` (`latest` = newest release). With [`compose.yaml`](https://github.com/spielkind/shop-notify/blob/main/compose.yaml) and your `.env`:
139
+
140
+ ```sh
141
+ mkdir -p data
142
+ docker compose pull
143
+ docker compose up -d
144
+ docker compose logs -f
145
+ ```
146
+
147
+ To use `notify.yml`, uncomment its volume in `compose.yaml` and set `NOTIFY_CONFIG=/config/notify.yml`; set `TZ=Europe/Berlin` (or your time zone) as the container runs in UTC otherwise. **→ [Docker in detail](https://github.com/spielkind/shop-notify/blob/main/docs/docker.md):** image tags, updates, moving an existing database, verifying the image, troubleshooting.
148
+
149
+ ### systemd
150
+ Alternatively install it as a sandboxed systemd service, e.g. on Raspberry Pi OS; `systemctl status` then shows the result of the last poll. **→ [systemd setup](https://github.com/spielkind/shop-notify/blob/main/docs/systemd.md)**
151
+
152
+ ### Locally
153
+ Install it from [PyPI](https://pypi.org/project/shop-notify/) with [uv](https://docs.astral.sh/uv/) (it downloads Python 3.14+ if needed) or [pipx](https://pipx.pypa.io/):
154
+
155
+ ```sh
156
+ uv tool install shop-notify # or: pipx install shop-notify
157
+ # create .env in the current directory (see .env.example) and fill it in
158
+ shop-notify
159
+ ```
160
+
161
+ Update with `uv tool upgrade shop-notify`. To run it from a clone of the repository instead: `uv sync`, then `uv run shop-notify`.
162
+
163
+ Stop it with `Ctrl+C`.
164
+
165
+ ### Health check
166
+ After every successful poll Shop Notify touches a heartbeat file. `shop-notify healthcheck` exits with `0` if the last successful poll is younger than `2 × POLL_INTERVAL + 60 s`, otherwise with `1` – e.g. when Etsy rejects the API key or the network is down. Docker uses it for its health status; manually:
167
+
168
+ ```sh
169
+ shop-notify healthcheck # local
170
+ docker compose exec shop-notify shop-notify healthcheck # Docker
171
+ ```
172
+
173
+ ## FAQ
174
+
175
+ <details>
176
+ <summary>Why not simply use an RSS feed?</summary>
177
+
178
+ Etsy doesn't provide an official, reliable feed of new shop listings. The Open API is the supported way to read them.
179
+ </details>
180
+
181
+ <details>
182
+ <summary>Every request fails with <code>403 API key not found or not active</code></summary>
183
+
184
+ Your Etsy app is still *Pending Personal Approval*, or keystring and shared secret are mixed up. Check the status under <https://www.etsy.com/developers/your-apps>.
185
+ </details>
186
+
187
+ <details>
188
+ <summary>I received the same item twice</summary>
189
+
190
+ That's intended if the item was gone from the shop in between (sold, deactivated, renewed) – re-appeared items are reported again. It also happens if all notification targets failed: then the items are retried with the next poll.
191
+ </details>
192
+
193
+ <details>
194
+ <summary>Can I watch several shops?</summary>
195
+
196
+ Not yet with a single instance ([planned](https://github.com/spielkind/shop-notify/issues/7)) – run one instance per shop with its own `.env` and `DB_PATH`. Keep the shared API quota in mind.
197
+ </details>
198
+
199
+ <details>
200
+ <summary>Why didn't I get notified about the items already in the shop?</summary>
201
+
202
+ On the [first run](#first-run) existing items are only recorded, so you are not flooded with notifications. Set `NOTIFY_EXISTING_ON_FIRST_RUN=true` and delete the database to receive them once.
203
+ </details>
204
+
205
+ ## Development
206
+
207
+ ```sh
208
+ uv sync
209
+ uv run pre-commit install
210
+ uv run pytest
211
+ ```
212
+
213
+ See [CONTRIBUTING.md](https://github.com/spielkind/shop-notify/blob/main/CONTRIBUTING.md) for the development workflow, pull requests and releases. Planned features are on the [roadmap](https://github.com/spielkind/shop-notify/issues?q=is%3Aopen+label%3Aroadmap); security issues please report as described in [SECURITY.md](https://github.com/spielkind/shop-notify/blob/main/SECURITY.md).
214
+
215
+ - [Architecture](https://github.com/spielkind/shop-notify/blob/main/docs/architecture.md) and [security model](https://github.com/spielkind/shop-notify/blob/main/docs/security.md)
216
+ - [Governance](https://github.com/spielkind/shop-notify/blob/main/GOVERNANCE.md) and [Code of Conduct](https://github.com/spielkind/shop-notify/blob/main/CODE_OF_CONDUCT.md)
217
+
218
+ ## License and disclaimer
219
+ [MIT](https://github.com/spielkind/shop-notify/blob/main/LICENSE).
220
+
221
+ The term "Etsy" is a trademark of Etsy, Inc. This application uses the Etsy API but is not endorsed or certified by Etsy, Inc.
@@ -0,0 +1,114 @@
1
+ [project]
2
+ name = "shop-notify"
3
+ version = "1.0.1"
4
+ description = "Watch an Etsy shop for new listings and send notifications."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.14"
8
+ keywords = [
9
+ "etsy",
10
+ "notifications",
11
+ "apprise",
12
+ "shop",
13
+ "new-listings",
14
+ "telegram",
15
+ "ntfy",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Environment :: Console",
20
+ "Environment :: No Input/Output (Daemon)",
21
+ "Intended Audience :: End Users/Desktop",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Operating System :: MacOS",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Programming Language :: Python :: 3.15",
28
+ "Topic :: Communications",
29
+ "Topic :: Office/Business",
30
+ ]
31
+ dependencies = [
32
+ "apprise>=2.0.1",
33
+ "httpx>=0.28",
34
+ "jinja2>=3.1.6",
35
+ "pydantic>=2.12",
36
+ "pydantic-settings>=2.11",
37
+ ]
38
+
39
+ [[project.authors]]
40
+ name = "Reik Keutterling"
41
+ email = "spielkind@gmail.com"
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/spielkind/shop-notify"
45
+ Documentation = "https://github.com/spielkind/shop-notify#readme"
46
+ Changelog = "https://github.com/spielkind/shop-notify/blob/main/CHANGELOG.md"
47
+ Issues = "https://github.com/spielkind/shop-notify/issues"
48
+ Source = "https://github.com/spielkind/shop-notify"
49
+
50
+ [project.scripts]
51
+ shop-notify = "shop_notify.__main__:main"
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "actionlint-py>=1.7.12.25",
56
+ "hypothesis>=6.170.5",
57
+ "mypy>=1.18",
58
+ "pip-audit>=2.10.1",
59
+ "pre-commit>=4.7.0",
60
+ "pytest>=8.4",
61
+ "pytest-cov>=7.1.0",
62
+ "respx>=0.22",
63
+ "ruff>=0.14",
64
+ "twine>=7.0.0",
65
+ "typos>=1.51.1",
66
+ "zizmor>=1.31.1",
67
+ ]
68
+
69
+ [build-system]
70
+ requires = ["uv_build>=0.9,<1"]
71
+ build-backend = "uv_build"
72
+
73
+ [tool.ruff]
74
+ line-length = 100
75
+
76
+ [tool.ruff.lint]
77
+ select = [
78
+ "E",
79
+ "W",
80
+ "F",
81
+ "I",
82
+ "B",
83
+ "UP",
84
+ "SIM",
85
+ "RUF",
86
+ "PL",
87
+ "N",
88
+ ]
89
+ ignore = [
90
+ "PLR2004",
91
+ "PLR0913",
92
+ ]
93
+
94
+ [tool.mypy]
95
+ strict = true
96
+ plugins = ["pydantic.mypy"]
97
+
98
+ [tool.pytest.ini_options]
99
+ testpaths = ["tests"]
100
+ addopts = "-ra --cov --cov-report=term-missing"
101
+ filterwarnings = ["error"]
102
+
103
+ [tool.coverage.run]
104
+ source = ["shop_notify"]
105
+ branch = true
106
+ relative_files = true
107
+
108
+ [tool.coverage.report]
109
+ fail_under = 100
110
+ show_missing = true
111
+ exclude_also = [
112
+ 'if __name__ == "__main__":',
113
+ 'assert_never\(',
114
+ ]