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.
- shop_notify-1.0.1/PKG-INFO +254 -0
- shop_notify-1.0.1/README.md +221 -0
- shop_notify-1.0.1/pyproject.toml +114 -0
- shop_notify-1.0.1/pyproject.toml.orig +86 -0
- shop_notify-1.0.1/src/shop_notify/__init__.py +5 -0
- shop_notify-1.0.1/src/shop_notify/__main__.py +371 -0
- shop_notify-1.0.1/src/shop_notify/config.py +81 -0
- shop_notify-1.0.1/src/shop_notify/db.py +72 -0
- shop_notify-1.0.1/src/shop_notify/etsy.py +211 -0
- shop_notify-1.0.1/src/shop_notify/models.py +43 -0
- shop_notify-1.0.1/src/shop_notify/notifications.py +353 -0
- shop_notify-1.0.1/src/shop_notify/systemd.py +54 -0
- shop_notify-1.0.1/src/shop_notify/templates/de/body.html.j2 +46 -0
- shop_notify-1.0.1/src/shop_notify/templates/de/body.txt.j2 +8 -0
- shop_notify-1.0.1/src/shop_notify/templates/de/item_body.txt.j2 +3 -0
- shop_notify-1.0.1/src/shop_notify/templates/de/item_subject.txt.j2 +1 -0
- shop_notify-1.0.1/src/shop_notify/templates/de/subject.txt.j2 +7 -0
- shop_notify-1.0.1/src/shop_notify/templates/en/body.html.j2 +46 -0
- shop_notify-1.0.1/src/shop_notify/templates/en/body.txt.j2 +8 -0
- shop_notify-1.0.1/src/shop_notify/templates/en/item_body.txt.j2 +3 -0
- shop_notify-1.0.1/src/shop_notify/templates/en/item_subject.txt.j2 +1 -0
- shop_notify-1.0.1/src/shop_notify/templates/en/subject.txt.j2 +7 -0
|
@@ -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
|
+
[](https://github.com/spielkind/shop-notify/actions/workflows/ci.yml)
|
|
42
|
+
[](https://github.com/spielkind/shop-notify/actions/workflows/docker.yml)
|
|
43
|
+
[](https://github.com/spielkind/shop-notify/tree/python-coverage-comment-action-data)
|
|
44
|
+
[](https://scorecard.dev/viewer/?uri=github.com/spielkind/shop-notify)
|
|
45
|
+
[](https://pypi.org/project/shop-notify/)
|
|
46
|
+
[](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
|
+
[](https://github.com/spielkind/shop-notify/actions/workflows/ci.yml)
|
|
9
|
+
[](https://github.com/spielkind/shop-notify/actions/workflows/docker.yml)
|
|
10
|
+
[](https://github.com/spielkind/shop-notify/tree/python-coverage-comment-action-data)
|
|
11
|
+
[](https://scorecard.dev/viewer/?uri=github.com/spielkind/shop-notify)
|
|
12
|
+
[](https://pypi.org/project/shop-notify/)
|
|
13
|
+
[](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
|
+
]
|