napper-sync 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.
- napper_sync-0.1.0/CHANGELOG.md +18 -0
- napper_sync-0.1.0/CONTRIBUTING.md +38 -0
- napper_sync-0.1.0/LICENSE +21 -0
- napper_sync-0.1.0/MANIFEST.in +8 -0
- napper_sync-0.1.0/PKG-INFO +223 -0
- napper_sync-0.1.0/README.md +194 -0
- napper_sync-0.1.0/docs/deployment.md +130 -0
- napper_sync-0.1.0/docs/famly-api.md +120 -0
- napper_sync-0.1.0/docs/napper-api.md +206 -0
- napper_sync-0.1.0/docs/releasing.md +52 -0
- napper_sync-0.1.0/examples/config.example.toml +25 -0
- napper_sync-0.1.0/examples/entries.example.csv +9 -0
- napper_sync-0.1.0/examples/entries.example.json +8 -0
- napper_sync-0.1.0/examples/systemd/napper-sync-failed@.service +14 -0
- napper_sync-0.1.0/examples/systemd/napper-sync.service +29 -0
- napper_sync-0.1.0/examples/systemd/napper-sync.timer +17 -0
- napper_sync-0.1.0/examples/windows-task.ps1 +6 -0
- napper_sync-0.1.0/pyproject.toml +55 -0
- napper_sync-0.1.0/setup.cfg +4 -0
- napper_sync-0.1.0/src/napper_sync/__init__.py +7 -0
- napper_sync-0.1.0/src/napper_sync/categories.py +314 -0
- napper_sync-0.1.0/src/napper_sync/cli.py +389 -0
- napper_sync-0.1.0/src/napper_sync/config.py +140 -0
- napper_sync-0.1.0/src/napper_sync/famly.py +157 -0
- napper_sync-0.1.0/src/napper_sync/famly_mapping.py +130 -0
- napper_sync-0.1.0/src/napper_sync/napper.py +451 -0
- napper_sync-0.1.0/src/napper_sync/notify.py +94 -0
- napper_sync-0.1.0/src/napper_sync/sources.py +81 -0
- napper_sync-0.1.0/src/napper_sync/sync.py +270 -0
- napper_sync-0.1.0/src/napper_sync.egg-info/PKG-INFO +223 -0
- napper_sync-0.1.0/src/napper_sync.egg-info/SOURCES.txt +45 -0
- napper_sync-0.1.0/src/napper_sync.egg-info/dependency_links.txt +1 -0
- napper_sync-0.1.0/src/napper_sync.egg-info/entry_points.txt +2 -0
- napper_sync-0.1.0/src/napper_sync.egg-info/requires.txt +4 -0
- napper_sync-0.1.0/src/napper_sync.egg-info/top_level.txt +1 -0
- napper_sync-0.1.0/tests/conftest.py +186 -0
- napper_sync-0.1.0/tests/test_categories.py +76 -0
- napper_sync-0.1.0/tests/test_famly_client.py +44 -0
- napper_sync-0.1.0/tests/test_famly_mapping.py +163 -0
- napper_sync-0.1.0/tests/test_napper_client.py +169 -0
- napper_sync-0.1.0/tests/test_notify_and_cli.py +122 -0
- napper_sync-0.1.0/tests/test_probe_corrections.py +86 -0
- napper_sync-0.1.0/tests/test_sync.py +308 -0
- napper_sync-0.1.0/tools/analyze_categories.py +100 -0
- napper_sync-0.1.0/tools/probe_corrections.py +167 -0
- napper_sync-0.1.0/tools/probe_write_path.py +159 -0
- napper_sync-0.1.0/tools/roundtrip_categories.py +131 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-11-08
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
- Napper client: OTP login, automatic token refresh, read/write/delete for all
|
|
8
|
+
eleven log categories, local field validation, 429 back-off.
|
|
9
|
+
- Famly client and mapping: calendar endpoint, sleep/meal/diaper → Napper,
|
|
10
|
+
unknown event types reported instead of dropped.
|
|
11
|
+
- Sync core with duplicate detection (10-minute fuzz, span overlap, one-per-day
|
|
12
|
+
markers), dry-run by default.
|
|
13
|
+
- Corrections: Famly entries carry a deterministic Napper ID, so a later change
|
|
14
|
+
at the nursery (end time, note, diaper content) updates the entry instead of
|
|
15
|
+
being skipped. CSV/JSON rows may pin an `id` for the same effect.
|
|
16
|
+
- CSV/JSON import.
|
|
17
|
+
- Discord notifications; systemd units and a Windows task example.
|
|
18
|
+
- Documentation of both APIs as observed.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking a look. Two things matter most here, because both APIs are
|
|
4
|
+
undocumented and change without notice:
|
|
5
|
+
|
|
6
|
+
1. **Verify against the real API before changing a request shape.** The mock
|
|
7
|
+
in `tests/conftest.py` reproduces what was observed; it cannot tell you
|
|
8
|
+
that Napper changed. `tools/roundtrip_categories.py` can - it writes one
|
|
9
|
+
entry per category far in the past and cleans up after itself.
|
|
10
|
+
2. **Never commit anything from `~/.config/napper-sync/`**, and strip IDs,
|
|
11
|
+
names and tokens from any API output you paste into an issue. Child IDs
|
|
12
|
+
and account IDs are UUIDs; the `.gitignore` catches the obvious files but
|
|
13
|
+
not a pasted JSON blob.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e ".[dev]"
|
|
19
|
+
pytest
|
|
20
|
+
ruff check . && ruff format .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Adding a Famly event type
|
|
24
|
+
|
|
25
|
+
1. Capture one real event with `napper-sync famly calendar <from> <to> --json`
|
|
26
|
+
and anonymise it (replace the child's name in `title`, drop `originator`
|
|
27
|
+
and `invitees`).
|
|
28
|
+
2. Add a branch in `famly_mapping.translate()` and a test in
|
|
29
|
+
`tests/test_famly_mapping.py` using the anonymised event.
|
|
30
|
+
3. If Napper needs a category or field that `categories.py` does not have,
|
|
31
|
+
confirm the field name against your own history
|
|
32
|
+
(`tools/analyze_categories.py`) first.
|
|
33
|
+
|
|
34
|
+
## Style
|
|
35
|
+
|
|
36
|
+
Ruff handles formatting and linting (`ruff format`, `ruff check`). Keep the
|
|
37
|
+
standard-library-only rule for the package itself; dev tooling may use
|
|
38
|
+
anything.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Wuestling
|
|
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,8 @@
|
|
|
1
|
+
# Make the source distribution self-contained: the tests need conftest.py and
|
|
2
|
+
# reach into tools/, and packagers want the docs and examples next to the code.
|
|
3
|
+
graft tests
|
|
4
|
+
graft tools
|
|
5
|
+
graft docs
|
|
6
|
+
graft examples
|
|
7
|
+
include CHANGELOG.md CONTRIBUTING.md
|
|
8
|
+
global-exclude __pycache__ *.py[cod] .coverage
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: napper-sync
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial Napper baby-tracker API client, and a sync that imports what the nursery logs in Famly.
|
|
5
|
+
Author: Patrick Wuestling
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PW-CODING/napper-sync
|
|
8
|
+
Project-URL: Issues, https://github.com/PW-CODING/napper-sync/issues
|
|
9
|
+
Project-URL: Documentation, https://github.com/PW-CODING/napper-sync/tree/main/docs
|
|
10
|
+
Project-URL: Changelog, https://github.com/PW-CODING/napper-sync/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: napper,famly,baby,baby-tracker,sleep-tracker,nursery,daycare,childcare,kita,krippe,sync,api-client
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Home Automation
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# napper-sync
|
|
31
|
+
|
|
32
|
+
**Sync what your nursery logs in [Famly](https://famly.co) - naps, meals, diaper
|
|
33
|
+
changes - into the [Napper](https://napper.app) baby tracker, automatically. Also an
|
|
34
|
+
unofficial Python client for the Napper API.**
|
|
35
|
+
|
|
36
|
+
[](https://github.com/PW-CODING/napper-sync/actions/workflows/ci.yml)
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
**Auf Deutsch:** Überträgt automatisch, was die Kita in Famly einträgt - Schlaf, Mahlzeiten
|
|
41
|
+
und Windelwechsel - in die Baby-App Napper. Kein Abtippen am Abend, keine Duplikate, und
|
|
42
|
+
nachträgliche Korrekturen der Kita werden übernommen. Läuft per Zeitplan auf einem
|
|
43
|
+
Raspberry Pi oder jedem anderen Rechner und braucht nur die Python-Standardbibliothek.
|
|
44
|
+
Die Dokumentation ist englisch, Fragen und Issues gern auch auf Deutsch.
|
|
45
|
+
|
|
46
|
+
Napper is a sleep-and-care tracker for babies. Famly is the app many nurseries
|
|
47
|
+
use to tell parents what happened during the day. If your child is in a
|
|
48
|
+
Famly nursery and you track at home with Napper, you end up typing the
|
|
49
|
+
nursery's entries into Napper by hand every evening. This tool does that for
|
|
50
|
+
you, every hour, without creating duplicates.
|
|
51
|
+
|
|
52
|
+
Along the way it documents two APIs that have no public documentation - see
|
|
53
|
+
[`docs/`](https://github.com/PW-CODING/napper-sync/tree/main/docs).
|
|
54
|
+
|
|
55
|
+
> **Disclaimer.** This project is not affiliated with, endorsed by, or supported by
|
|
56
|
+
> Napper AB or Famly ApS. It talks to private, undocumented APIs that the official
|
|
57
|
+
> apps use internally. Those APIs may change or disappear with any app update, and
|
|
58
|
+
> using them may not be covered by the services' terms - check the terms that apply
|
|
59
|
+
> to your account before you decide. Use it with your own account, for your own
|
|
60
|
+
> child, at your own risk; how you use it is your responsibility.
|
|
61
|
+
|
|
62
|
+
## What it does
|
|
63
|
+
|
|
64
|
+
- **Napper API client** - log in with the e-mailed one-time code, read the
|
|
65
|
+
history, write and delete entries in all eleven categories Napper knows
|
|
66
|
+
(nap, bedtime, woke up, night waking, nursing, bottle, solids, pumping,
|
|
67
|
+
diaper, temperature, medicine), with field validation before anything is sent.
|
|
68
|
+
- **Famly → Napper sync** - reads the nursery's daily events and creates the
|
|
69
|
+
matching Napper entries, marked `(Famly)` so you can tell them apart. When
|
|
70
|
+
the nursery corrects an entry later, the Napper entry follows.
|
|
71
|
+
- **Safe by default** - every write is a dry run unless you pass `--live`;
|
|
72
|
+
duplicate detection makes repeated runs harmless; the Napper rate limit is
|
|
73
|
+
handled with back-off.
|
|
74
|
+
- **File import** - CSV or JSON for anything else you want to get into Napper.
|
|
75
|
+
- **Unattended operation** - systemd timer, Discord notifications on failure
|
|
76
|
+
and on successful imports, tokens refreshed automatically.
|
|
77
|
+
|
|
78
|
+
Standard library only; no runtime dependencies.
|
|
79
|
+
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pipx install git+https://github.com/PW-CODING/napper-sync
|
|
84
|
+
# or
|
|
85
|
+
pip install --user git+https://github.com/PW-CODING/napper-sync
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Requires Python 3.11+.
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
napper-sync login you@example.com # Napper e-mails a code; paste it
|
|
94
|
+
napper-sync babies # find your baby's ID
|
|
95
|
+
napper-sync config init # write ~/.config/napper-sync/config.toml
|
|
96
|
+
# -> put the baby ID into [napper] baby_id
|
|
97
|
+
|
|
98
|
+
napper-sync logs 2026-09-01 2026-09-07 # read a week
|
|
99
|
+
napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 # dry run
|
|
100
|
+
napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 --live # write
|
|
101
|
+
napper-sync add diaper 2026-09-18T15:30 --set diaperContent=WET --live
|
|
102
|
+
napper-sync add medicine 2026-09-18T19:00 --comment "Vitamin D" --live
|
|
103
|
+
napper-sync categories # every category, field and allowed value
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Famly sync
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
napper-sync famly token <VALUE> # from localStorage["famly.accessToken"] in a logged-in browser
|
|
110
|
+
napper-sync famly children # -> [famly] child_id in the config
|
|
111
|
+
napper-sync famly calendar 2026-09-14 2026-09-18 # what the nursery logged
|
|
112
|
+
|
|
113
|
+
napper-sync sync-famly # dry run: shows what would be written
|
|
114
|
+
napper-sync sync-famly --live # write it
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To run this hourly on a Raspberry Pi with Discord alerts, see
|
|
118
|
+
[`docs/deployment.md`](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md).
|
|
119
|
+
|
|
120
|
+
### Importing a file
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
napper-sync sync-file entries.csv --live
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Columns `start,end,category,comment` plus any category field as a further
|
|
127
|
+
column; see [`examples/entries.example.csv`](https://github.com/PW-CODING/napper-sync/blob/main/examples/entries.example.csv).
|
|
128
|
+
An optional `id` column pins the Napper ID, so importing a corrected file
|
|
129
|
+
updates the entries instead of duplicating them.
|
|
130
|
+
Categories accept English or German short names (`nap`/`nickerchen`,
|
|
131
|
+
`diaper`/`windel`, `bottle`/`flasche`, …).
|
|
132
|
+
|
|
133
|
+
## How it stays safe
|
|
134
|
+
|
|
135
|
+
| Risk | Answer |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Writing by accident | every write and delete is a dry run without `--live` |
|
|
138
|
+
| Duplicates from repeated runs | Famly entries get a stable ID derived from child, category and start, so a repeated run rewrites the same entry instead of adding one; everything else is skipped if the same category exists within 10 minutes or overlaps; day markers are one-per-day |
|
|
139
|
+
| Corrections at the nursery | a changed end time, note or diaper content overwrites the Napper entry on the next run; fields added by hand in the app are kept |
|
|
140
|
+
| Hammering the API | 429 responses are parsed and retried with the server's requested delay |
|
|
141
|
+
| Bad data | field names, enum values and ranges are validated locally before a request is built |
|
|
142
|
+
| Silent breakage | Discord message on expired token, failed write, crashed run; unknown Famly event types are listed, never dropped |
|
|
143
|
+
|
|
144
|
+
## Project layout
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
src/napper_sync/
|
|
148
|
+
napper.py Napper API client (auth, read, write, delete)
|
|
149
|
+
categories.py what each category expects - the data model
|
|
150
|
+
sync.py Entry, duplicate detection, the sync loop
|
|
151
|
+
sources.py CSV / JSON sources
|
|
152
|
+
famly.py Famly API client
|
|
153
|
+
famly_mapping.py Famly events -> Napper entries
|
|
154
|
+
notify.py Discord webhook
|
|
155
|
+
config.py ~/.config/napper-sync/
|
|
156
|
+
cli.py the napper-sync command
|
|
157
|
+
tools/ research scripts: analyse history, rediscover endpoints, round-trip and correction checks
|
|
158
|
+
examples/ config, data files, systemd units, Windows task
|
|
159
|
+
docs/ what is known about both APIs, and how to deploy
|
|
160
|
+
tests/ pytest suite against an in-process mock of the Napper API
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Documentation
|
|
164
|
+
|
|
165
|
+
- [docs/napper-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/napper-api.md) - endpoints, auth, the log object,
|
|
166
|
+
all categories and fields, the traps (seconds in a field called minutes,
|
|
167
|
+
percent-encoded comments, date-based IDs), and how the write path was found
|
|
168
|
+
- [docs/famly-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/famly-api.md) - instances, token, the calendar
|
|
169
|
+
endpoint and its event shapes
|
|
170
|
+
- [docs/deployment.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md) - systemd timer, hardening, Discord
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
git clone https://github.com/PW-CODING/napper-sync && cd napper-sync
|
|
176
|
+
pip install -e ".[dev]"
|
|
177
|
+
pytest
|
|
178
|
+
ruff check . && ruff format --check .
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The tests run against an in-process HTTP mock that reproduces the real API's
|
|
182
|
+
quirks (whole-day responses on write, 429 throttling, token refresh). Nothing
|
|
183
|
+
touches a real account.
|
|
184
|
+
|
|
185
|
+
Tools under `tools/` do talk to the real API and are meant for the day an app
|
|
186
|
+
update breaks something: `probe_write_path.py` rediscovers the write endpoint,
|
|
187
|
+
`roundtrip_categories.py` writes one entry per category far in the past,
|
|
188
|
+
checks it and deletes it again, `analyze_categories.py` summarises which
|
|
189
|
+
fields your own history actually uses, and `probe_corrections.py` checks that
|
|
190
|
+
a correction made later at the nursery updates the Napper entry in place.
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
Issues and pull requests are welcome, in English or German. If an app update
|
|
195
|
+
changed something, the most useful report includes the output of
|
|
196
|
+
`tools/roundtrip_categories.py` or of `napper-sync famly calendar` for a day
|
|
197
|
+
with the new event type (with names and IDs removed).
|
|
198
|
+
|
|
199
|
+
## Related projects
|
|
200
|
+
|
|
201
|
+
Community projects around the same two apps, none of which connects them:
|
|
202
|
+
|
|
203
|
+
- [home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper) -
|
|
204
|
+
read-only Napper integration for Home Assistant
|
|
205
|
+
- [ha-napper](https://github.com/bbruenings/ha-napper) - another Home Assistant
|
|
206
|
+
integration for Napper
|
|
207
|
+
- [napper-export](https://github.com/brittraee/napper-export) - export a child's
|
|
208
|
+
complete Napper history
|
|
209
|
+
- [famly-fetch](https://github.com/jacobbunk/famly-fetch) - download your child's
|
|
210
|
+
photos from Famly
|
|
211
|
+
|
|
212
|
+
## Acknowledgements
|
|
213
|
+
|
|
214
|
+
Reading the Napper API was first worked out by
|
|
215
|
+
[brittraee/napper-export](https://github.com/brittraee/napper-export) and
|
|
216
|
+
[g-nogueira/home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper);
|
|
217
|
+
the OTP login flow and request headers here follow their findings. Writing,
|
|
218
|
+
deleting, the category model and the Famly side were worked out for this
|
|
219
|
+
project.
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
[MIT](https://github.com/PW-CODING/napper-sync/blob/main/LICENSE)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# napper-sync
|
|
2
|
+
|
|
3
|
+
**Sync what your nursery logs in [Famly](https://famly.co) - naps, meals, diaper
|
|
4
|
+
changes - into the [Napper](https://napper.app) baby tracker, automatically. Also an
|
|
5
|
+
unofficial Python client for the Napper API.**
|
|
6
|
+
|
|
7
|
+
[](https://github.com/PW-CODING/napper-sync/actions/workflows/ci.yml)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
**Auf Deutsch:** Überträgt automatisch, was die Kita in Famly einträgt - Schlaf, Mahlzeiten
|
|
12
|
+
und Windelwechsel - in die Baby-App Napper. Kein Abtippen am Abend, keine Duplikate, und
|
|
13
|
+
nachträgliche Korrekturen der Kita werden übernommen. Läuft per Zeitplan auf einem
|
|
14
|
+
Raspberry Pi oder jedem anderen Rechner und braucht nur die Python-Standardbibliothek.
|
|
15
|
+
Die Dokumentation ist englisch, Fragen und Issues gern auch auf Deutsch.
|
|
16
|
+
|
|
17
|
+
Napper is a sleep-and-care tracker for babies. Famly is the app many nurseries
|
|
18
|
+
use to tell parents what happened during the day. If your child is in a
|
|
19
|
+
Famly nursery and you track at home with Napper, you end up typing the
|
|
20
|
+
nursery's entries into Napper by hand every evening. This tool does that for
|
|
21
|
+
you, every hour, without creating duplicates.
|
|
22
|
+
|
|
23
|
+
Along the way it documents two APIs that have no public documentation - see
|
|
24
|
+
[`docs/`](https://github.com/PW-CODING/napper-sync/tree/main/docs).
|
|
25
|
+
|
|
26
|
+
> **Disclaimer.** This project is not affiliated with, endorsed by, or supported by
|
|
27
|
+
> Napper AB or Famly ApS. It talks to private, undocumented APIs that the official
|
|
28
|
+
> apps use internally. Those APIs may change or disappear with any app update, and
|
|
29
|
+
> using them may not be covered by the services' terms - check the terms that apply
|
|
30
|
+
> to your account before you decide. Use it with your own account, for your own
|
|
31
|
+
> child, at your own risk; how you use it is your responsibility.
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
- **Napper API client** - log in with the e-mailed one-time code, read the
|
|
36
|
+
history, write and delete entries in all eleven categories Napper knows
|
|
37
|
+
(nap, bedtime, woke up, night waking, nursing, bottle, solids, pumping,
|
|
38
|
+
diaper, temperature, medicine), with field validation before anything is sent.
|
|
39
|
+
- **Famly → Napper sync** - reads the nursery's daily events and creates the
|
|
40
|
+
matching Napper entries, marked `(Famly)` so you can tell them apart. When
|
|
41
|
+
the nursery corrects an entry later, the Napper entry follows.
|
|
42
|
+
- **Safe by default** - every write is a dry run unless you pass `--live`;
|
|
43
|
+
duplicate detection makes repeated runs harmless; the Napper rate limit is
|
|
44
|
+
handled with back-off.
|
|
45
|
+
- **File import** - CSV or JSON for anything else you want to get into Napper.
|
|
46
|
+
- **Unattended operation** - systemd timer, Discord notifications on failure
|
|
47
|
+
and on successful imports, tokens refreshed automatically.
|
|
48
|
+
|
|
49
|
+
Standard library only; no runtime dependencies.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pipx install git+https://github.com/PW-CODING/napper-sync
|
|
55
|
+
# or
|
|
56
|
+
pip install --user git+https://github.com/PW-CODING/napper-sync
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Requires Python 3.11+.
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
napper-sync login you@example.com # Napper e-mails a code; paste it
|
|
65
|
+
napper-sync babies # find your baby's ID
|
|
66
|
+
napper-sync config init # write ~/.config/napper-sync/config.toml
|
|
67
|
+
# -> put the baby ID into [napper] baby_id
|
|
68
|
+
|
|
69
|
+
napper-sync logs 2026-09-01 2026-09-07 # read a week
|
|
70
|
+
napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 # dry run
|
|
71
|
+
napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 --live # write
|
|
72
|
+
napper-sync add diaper 2026-09-18T15:30 --set diaperContent=WET --live
|
|
73
|
+
napper-sync add medicine 2026-09-18T19:00 --comment "Vitamin D" --live
|
|
74
|
+
napper-sync categories # every category, field and allowed value
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Famly sync
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
napper-sync famly token <VALUE> # from localStorage["famly.accessToken"] in a logged-in browser
|
|
81
|
+
napper-sync famly children # -> [famly] child_id in the config
|
|
82
|
+
napper-sync famly calendar 2026-09-14 2026-09-18 # what the nursery logged
|
|
83
|
+
|
|
84
|
+
napper-sync sync-famly # dry run: shows what would be written
|
|
85
|
+
napper-sync sync-famly --live # write it
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To run this hourly on a Raspberry Pi with Discord alerts, see
|
|
89
|
+
[`docs/deployment.md`](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md).
|
|
90
|
+
|
|
91
|
+
### Importing a file
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
napper-sync sync-file entries.csv --live
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Columns `start,end,category,comment` plus any category field as a further
|
|
98
|
+
column; see [`examples/entries.example.csv`](https://github.com/PW-CODING/napper-sync/blob/main/examples/entries.example.csv).
|
|
99
|
+
An optional `id` column pins the Napper ID, so importing a corrected file
|
|
100
|
+
updates the entries instead of duplicating them.
|
|
101
|
+
Categories accept English or German short names (`nap`/`nickerchen`,
|
|
102
|
+
`diaper`/`windel`, `bottle`/`flasche`, …).
|
|
103
|
+
|
|
104
|
+
## How it stays safe
|
|
105
|
+
|
|
106
|
+
| Risk | Answer |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Writing by accident | every write and delete is a dry run without `--live` |
|
|
109
|
+
| Duplicates from repeated runs | Famly entries get a stable ID derived from child, category and start, so a repeated run rewrites the same entry instead of adding one; everything else is skipped if the same category exists within 10 minutes or overlaps; day markers are one-per-day |
|
|
110
|
+
| Corrections at the nursery | a changed end time, note or diaper content overwrites the Napper entry on the next run; fields added by hand in the app are kept |
|
|
111
|
+
| Hammering the API | 429 responses are parsed and retried with the server's requested delay |
|
|
112
|
+
| Bad data | field names, enum values and ranges are validated locally before a request is built |
|
|
113
|
+
| Silent breakage | Discord message on expired token, failed write, crashed run; unknown Famly event types are listed, never dropped |
|
|
114
|
+
|
|
115
|
+
## Project layout
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
src/napper_sync/
|
|
119
|
+
napper.py Napper API client (auth, read, write, delete)
|
|
120
|
+
categories.py what each category expects - the data model
|
|
121
|
+
sync.py Entry, duplicate detection, the sync loop
|
|
122
|
+
sources.py CSV / JSON sources
|
|
123
|
+
famly.py Famly API client
|
|
124
|
+
famly_mapping.py Famly events -> Napper entries
|
|
125
|
+
notify.py Discord webhook
|
|
126
|
+
config.py ~/.config/napper-sync/
|
|
127
|
+
cli.py the napper-sync command
|
|
128
|
+
tools/ research scripts: analyse history, rediscover endpoints, round-trip and correction checks
|
|
129
|
+
examples/ config, data files, systemd units, Windows task
|
|
130
|
+
docs/ what is known about both APIs, and how to deploy
|
|
131
|
+
tests/ pytest suite against an in-process mock of the Napper API
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Documentation
|
|
135
|
+
|
|
136
|
+
- [docs/napper-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/napper-api.md) - endpoints, auth, the log object,
|
|
137
|
+
all categories and fields, the traps (seconds in a field called minutes,
|
|
138
|
+
percent-encoded comments, date-based IDs), and how the write path was found
|
|
139
|
+
- [docs/famly-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/famly-api.md) - instances, token, the calendar
|
|
140
|
+
endpoint and its event shapes
|
|
141
|
+
- [docs/deployment.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md) - systemd timer, hardening, Discord
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git clone https://github.com/PW-CODING/napper-sync && cd napper-sync
|
|
147
|
+
pip install -e ".[dev]"
|
|
148
|
+
pytest
|
|
149
|
+
ruff check . && ruff format --check .
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The tests run against an in-process HTTP mock that reproduces the real API's
|
|
153
|
+
quirks (whole-day responses on write, 429 throttling, token refresh). Nothing
|
|
154
|
+
touches a real account.
|
|
155
|
+
|
|
156
|
+
Tools under `tools/` do talk to the real API and are meant for the day an app
|
|
157
|
+
update breaks something: `probe_write_path.py` rediscovers the write endpoint,
|
|
158
|
+
`roundtrip_categories.py` writes one entry per category far in the past,
|
|
159
|
+
checks it and deletes it again, `analyze_categories.py` summarises which
|
|
160
|
+
fields your own history actually uses, and `probe_corrections.py` checks that
|
|
161
|
+
a correction made later at the nursery updates the Napper entry in place.
|
|
162
|
+
|
|
163
|
+
## Contributing
|
|
164
|
+
|
|
165
|
+
Issues and pull requests are welcome, in English or German. If an app update
|
|
166
|
+
changed something, the most useful report includes the output of
|
|
167
|
+
`tools/roundtrip_categories.py` or of `napper-sync famly calendar` for a day
|
|
168
|
+
with the new event type (with names and IDs removed).
|
|
169
|
+
|
|
170
|
+
## Related projects
|
|
171
|
+
|
|
172
|
+
Community projects around the same two apps, none of which connects them:
|
|
173
|
+
|
|
174
|
+
- [home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper) -
|
|
175
|
+
read-only Napper integration for Home Assistant
|
|
176
|
+
- [ha-napper](https://github.com/bbruenings/ha-napper) - another Home Assistant
|
|
177
|
+
integration for Napper
|
|
178
|
+
- [napper-export](https://github.com/brittraee/napper-export) - export a child's
|
|
179
|
+
complete Napper history
|
|
180
|
+
- [famly-fetch](https://github.com/jacobbunk/famly-fetch) - download your child's
|
|
181
|
+
photos from Famly
|
|
182
|
+
|
|
183
|
+
## Acknowledgements
|
|
184
|
+
|
|
185
|
+
Reading the Napper API was first worked out by
|
|
186
|
+
[brittraee/napper-export](https://github.com/brittraee/napper-export) and
|
|
187
|
+
[g-nogueira/home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper);
|
|
188
|
+
the OTP login flow and request headers here follow their findings. Writing,
|
|
189
|
+
deleting, the category model and the Famly side were worked out for this
|
|
190
|
+
project.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
[MIT](https://github.com/PW-CODING/napper-sync/blob/main/LICENSE)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Running napper-sync unattended
|
|
2
|
+
|
|
3
|
+
The intended setup is a small always-on Linux box (a Raspberry Pi is plenty;
|
|
4
|
+
a run takes about two seconds of CPU) with a systemd timer. A Windows
|
|
5
|
+
scheduled task works too and is described at the end.
|
|
6
|
+
|
|
7
|
+
## 1. Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# as the user that will run the sync (not root)
|
|
11
|
+
pipx install git+https://github.com/PW-CODING/napper-sync
|
|
12
|
+
# or: pip install --user git+https://github.com/PW-CODING/napper-sync
|
|
13
|
+
napper-sync --version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Python 3.11 or newer is required (Debian 12 "bookworm" ships 3.11).
|
|
17
|
+
|
|
18
|
+
## 2. Configure
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
napper-sync config init # writes ~/.config/napper-sync/config.toml
|
|
22
|
+
napper-sync login you@example.com # e-mailed one-time code
|
|
23
|
+
napper-sync babies # -> [napper] baby_id
|
|
24
|
+
napper-sync famly token <VALUE> # see "Getting the Famly token"
|
|
25
|
+
napper-sync famly children # -> [famly] child_id
|
|
26
|
+
napper-sync config show # everything in place?
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then dry-run it once and read the output:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
napper-sync sync-famly # shows what would be written
|
|
33
|
+
napper-sync sync-famly --live # writes
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Getting the Famly token
|
|
37
|
+
|
|
38
|
+
Log in to Famly in a desktop browser, open the developer tools (F12),
|
|
39
|
+
Console tab, and run:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
JSON.parse(localStorage["famly.accessToken"])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Copy the string (36 characters) into `napper-sync famly token <VALUE>`. If
|
|
46
|
+
your nursery is on a different Famly instance than `app.famly.de`, check
|
|
47
|
+
`localStorage["famly.existingLogins"]` and set `[famly] host` accordingly.
|
|
48
|
+
|
|
49
|
+
On a headless server, run the browser step on any machine and copy the token
|
|
50
|
+
across (`ssh host 'napper-sync famly token <VALUE>'`).
|
|
51
|
+
|
|
52
|
+
### Secrets and permissions
|
|
53
|
+
|
|
54
|
+
Everything sensitive sits in `~/.config/napper-sync/` with mode `600`:
|
|
55
|
+
`napper-tokens.json`, `famly-token`, and the Discord webhook inside
|
|
56
|
+
`config.toml`. Keep that directory out of backups that leave your control, and
|
|
57
|
+
never commit it.
|
|
58
|
+
|
|
59
|
+
## 3. systemd timer
|
|
60
|
+
|
|
61
|
+
Copy the three units from `examples/systemd/`, adjust `User=` and the paths in
|
|
62
|
+
`ExecStart=` / `ReadWritePaths=` to your installation, then:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
sudo install -m 644 examples/systemd/napper-sync.service \
|
|
66
|
+
examples/systemd/napper-sync.timer \
|
|
67
|
+
'examples/systemd/napper-sync-failed@.service' /etc/systemd/system/
|
|
68
|
+
sudo systemctl daemon-reload
|
|
69
|
+
sudo systemctl enable --now napper-sync.timer
|
|
70
|
+
|
|
71
|
+
systemctl list-timers napper-sync.timer # next run
|
|
72
|
+
sudo systemctl start napper-sync.service # run once, now
|
|
73
|
+
journalctl -u napper-sync -n 30 --no-pager # what happened
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Design notes, in case you want to change them:
|
|
77
|
+
|
|
78
|
+
* **`OnCalendar=Mon..Fri 07..18:00`** - hourly during nursery hours plus one
|
|
79
|
+
run after closing. Nothing is logged at night or on weekends, so nothing
|
|
80
|
+
runs. `systemd-analyze calendar 'Mon..Fri 07..18:00'` previews the schedule.
|
|
81
|
+
* **`[sync] days = 4`** - each run looks four days back. Three would make the
|
|
82
|
+
Monday run reach exactly to Friday; four leaves room for entries added
|
|
83
|
+
Friday evening.
|
|
84
|
+
* **`Persistent=true`** - runs missed while the machine was off are caught up
|
|
85
|
+
on boot.
|
|
86
|
+
* **`OnFailure=`** - if the process crashes or times out, a second unit posts
|
|
87
|
+
the last journal lines to Discord. API-level failures (expired token,
|
|
88
|
+
unreachable host) are reported by the sync itself.
|
|
89
|
+
* **Hardening** - `ProtectSystem=strict` and `ProtectHome=read-only`, with
|
|
90
|
+
`ReadWritePaths=` opened only for the config directory. That last line is
|
|
91
|
+
not optional: the Napper id-token is refreshed automatically and written
|
|
92
|
+
back to `napper-tokens.json`. Without write access the refresh fails
|
|
93
|
+
silently a few weeks after installation.
|
|
94
|
+
|
|
95
|
+
## 4. Discord notifications
|
|
96
|
+
|
|
97
|
+
Create a webhook in your Discord server (channel settings → Integrations →
|
|
98
|
+
Webhooks), put the URL into `[notify] discord_webhook`, and test:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
napper-sync notify # "Test message"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
You will then hear about:
|
|
105
|
+
|
|
106
|
+
* an expired Famly token (with the command to fix it),
|
|
107
|
+
* Famly or Napper being unreachable or rejecting a write,
|
|
108
|
+
* a crashed or timed-out run (via the `OnFailure=` unit, journal attached),
|
|
109
|
+
* every run that imported or corrected something, with the list of entries.
|
|
110
|
+
|
|
111
|
+
A run that finds nothing new stays silent.
|
|
112
|
+
|
|
113
|
+
What the webhook cannot tell you: that the machine is off or offline. Silence
|
|
114
|
+
is not proof that all is well.
|
|
115
|
+
|
|
116
|
+
## Windows instead of Linux
|
|
117
|
+
|
|
118
|
+
`examples/windows-task.ps1` registers an hourly scheduled task that runs
|
|
119
|
+
`napper-sync sync-famly --live`. Output goes nowhere by default; if you want
|
|
120
|
+
a log, wrap the call in a `.cmd` that appends to a file. The Discord
|
|
121
|
+
notifications work the same way.
|
|
122
|
+
|
|
123
|
+
## Updating
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pipx upgrade napper-sync # or: pip install --user -U git+https://github.com/PW-CODING/napper-sync
|
|
127
|
+
sudo systemctl start napper-sync.service && journalctl -u napper-sync -n 20
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Config and tokens are untouched by upgrades.
|