intervalssync 0.9.3__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.
Files changed (76) hide show
  1. intervalssync-0.9.3/.env.example +19 -0
  2. intervalssync-0.9.3/.github/FUNDING.yml +1 -0
  3. intervalssync-0.9.3/.github/pull_request_template.md +27 -0
  4. intervalssync-0.9.3/.github/workflows/release.yml +235 -0
  5. intervalssync-0.9.3/.github/workflows/test.yml +25 -0
  6. intervalssync-0.9.3/.gitignore +19 -0
  7. intervalssync-0.9.3/.python-version +1 -0
  8. intervalssync-0.9.3/CLAUDE.md +67 -0
  9. intervalssync-0.9.3/CONTRIBUTING.md +156 -0
  10. intervalssync-0.9.3/LICENSE +21 -0
  11. intervalssync-0.9.3/PKG-INFO +179 -0
  12. intervalssync-0.9.3/README.md +158 -0
  13. intervalssync-0.9.3/assets/icon.png +0 -0
  14. intervalssync-0.9.3/docs/AGENT.md +191 -0
  15. intervalssync-0.9.3/docs/img/demo.gif +0 -0
  16. intervalssync-0.9.3/docs/img/settings.png +0 -0
  17. intervalssync-0.9.3/docs/img/sync.png +0 -0
  18. intervalssync-0.9.3/main.py +21 -0
  19. intervalssync-0.9.3/pyproject.toml +66 -0
  20. intervalssync-0.9.3/src/intervalssync/__init__.py +3 -0
  21. intervalssync-0.9.3/src/intervalssync/_dropbox_app_key.py +7 -0
  22. intervalssync-0.9.3/src/intervalssync/_version.py +1 -0
  23. intervalssync-0.9.3/src/intervalssync/bryton/__init__.py +1 -0
  24. intervalssync-0.9.3/src/intervalssync/bryton/api.py +49 -0
  25. intervalssync-0.9.3/src/intervalssync/bryton/core.py +249 -0
  26. intervalssync-0.9.3/src/intervalssync/bryton/ddp.py +231 -0
  27. intervalssync-0.9.3/src/intervalssync/bryton/exceptions.py +13 -0
  28. intervalssync-0.9.3/src/intervalssync/bryton/fit_encode.py +363 -0
  29. intervalssync-0.9.3/src/intervalssync/bryton/workout.py +342 -0
  30. intervalssync-0.9.3/src/intervalssync/cli/__init__.py +5 -0
  31. intervalssync-0.9.3/src/intervalssync/cli/config.py +53 -0
  32. intervalssync-0.9.3/src/intervalssync/cli/env.py +189 -0
  33. intervalssync-0.9.3/src/intervalssync/cli/main.py +637 -0
  34. intervalssync-0.9.3/src/intervalssync/dropbox_client.py +143 -0
  35. intervalssync-0.9.3/src/intervalssync/gui/__init__.py +1 -0
  36. intervalssync-0.9.3/src/intervalssync/gui/app.py +352 -0
  37. intervalssync-0.9.3/src/intervalssync/gui/config.py +102 -0
  38. intervalssync-0.9.3/src/intervalssync/gui/profile_sync_ui.py +225 -0
  39. intervalssync-0.9.3/src/intervalssync/gui/secrets.py +135 -0
  40. intervalssync-0.9.3/src/intervalssync/gui/settings_view.py +716 -0
  41. intervalssync-0.9.3/src/intervalssync/gui/support_gamification.py +658 -0
  42. intervalssync-0.9.3/src/intervalssync/gui/sync_view.py +486 -0
  43. intervalssync-0.9.3/src/intervalssync/gui/system.py +21 -0
  44. intervalssync-0.9.3/src/intervalssync/gui/theme.py +216 -0
  45. intervalssync-0.9.3/src/intervalssync/gui/update_check.py +55 -0
  46. intervalssync-0.9.3/src/intervalssync/igpsport/__init__.py +1 -0
  47. intervalssync-0.9.3/src/intervalssync/igpsport/core.py +520 -0
  48. intervalssync-0.9.3/src/intervalssync/igpsport/interval_info.py +269 -0
  49. intervalssync-0.9.3/src/intervalssync/igpsport/profile_sync.py +391 -0
  50. intervalssync-0.9.3/src/intervalssync/igpsport/region.py +80 -0
  51. intervalssync-0.9.3/src/intervalssync/igpsport/workout.py +503 -0
  52. intervalssync-0.9.3/src/intervalssync/igpsport/zone_map.py +143 -0
  53. intervalssync-0.9.3/src/intervalssync/intervals_icu.py +201 -0
  54. intervalssync-0.9.3/tests/fixtures/bryton_workouts/Foundation_2.fit +0 -0
  55. intervalssync-0.9.3/tests/fixtures/bryton_workouts/Foundation_2.json +94 -0
  56. intervalssync-0.9.3/tests/fixtures/bryton_workouts/app.fit +0 -0
  57. intervalssync-0.9.3/tests/fixtures/bryton_workouts/app.json +101 -0
  58. intervalssync-0.9.3/tests/fixtures/bryton_workouts/web_bsWO0618_011758.fit +0 -0
  59. intervalssync-0.9.3/tests/fixtures/bryton_workouts/web_bsWO0618_011758.json +112 -0
  60. intervalssync-0.9.3/tests/test_bryton_core.py +313 -0
  61. intervalssync-0.9.3/tests/test_bryton_fit_encode.py +291 -0
  62. intervalssync-0.9.3/tests/test_bryton_fit_native.py +52 -0
  63. intervalssync-0.9.3/tests/test_bryton_workout.py +287 -0
  64. intervalssync-0.9.3/tests/test_cli.py +390 -0
  65. intervalssync-0.9.3/tests/test_config.py +172 -0
  66. intervalssync-0.9.3/tests/test_dropbox_client.py +120 -0
  67. intervalssync-0.9.3/tests/test_gui_app.py +35 -0
  68. intervalssync-0.9.3/tests/test_igpsport_core.py +608 -0
  69. intervalssync-0.9.3/tests/test_intervals_icu.py +119 -0
  70. intervalssync-0.9.3/tests/test_profile_sync.py +380 -0
  71. intervalssync-0.9.3/tests/test_secrets.py +147 -0
  72. intervalssync-0.9.3/tests/test_support_gamification.py +92 -0
  73. intervalssync-0.9.3/tests/test_update_check.py +61 -0
  74. intervalssync-0.9.3/tests/test_workout.py +400 -0
  75. intervalssync-0.9.3/tests/test_zone_map.py +99 -0
  76. intervalssync-0.9.3/uv.lock +366 -0
@@ -0,0 +1,19 @@
1
+ # Local development only. Copy to .env and run:
2
+ # uv run --env-file .env intervalssync-gui # GUI (uv loads env)
3
+ # uv run intervalssync sync --env-file .env # iGPSPORT → intervals.icu
4
+ # uv run intervalssync sync --source bryton --env-file .env # Bryton → intervals.icu
5
+ # uv run intervalssync upload-workouts --env-file .env # intervals.icu → iGPSPORT workouts
6
+ # uv run intervalssync sync-zones --env-file .env # intervals.icu → iGPSPORT profile
7
+ #
8
+ # iGPSPORT sync requires INTERVALSSYNC_IGPSPORT_* + INTERVALSSYNC_INTERVALS_API_KEY.
9
+ # Bryton sync requires INTERVALSSYNC_BRYTON_* + INTERVALSSYNC_INTERVALS_API_KEY.
10
+ # IGPSYNC_DROPBOX_APP_KEY is optional (Dropbox secondary upload in the GUI).
11
+ IGPSYNC_DROPBOX_APP_KEY=
12
+
13
+ INTERVALSSYNC_IGPSPORT_USER=
14
+ INTERVALSSYNC_IGPSPORT_PASSWORD=
15
+ # international (default) or china — use china for app.igpsport.cn accounts
16
+ INTERVALSSYNC_IGPSPORT_REGION=international
17
+ INTERVALSSYNC_BRYTON_EMAIL=
18
+ INTERVALSSYNC_BRYTON_PASSWORD=
19
+ INTERVALSSYNC_INTERVALS_API_KEY=
@@ -0,0 +1 @@
1
+ ko_fi: jorge_huxley
@@ -0,0 +1,27 @@
1
+ <!--
2
+ Title (the field above): one imperative line with a Conventional Commits prefix,
3
+ e.g. feat(android): build and publish an APK on release
4
+ Common prefixes: feat, fix, ci, docs, refactor, test, chore, build, perf
5
+ -->
6
+
7
+ ## Summary
8
+
9
+ <!-- 2-3 sentences: what changed and *why*. The motivation matters more than the
10
+ diff, which reviewers can read themselves. -->
11
+
12
+ ## Changes
13
+
14
+ <!-- Short bullets, grouped by area. -->
15
+ -
16
+
17
+ ## Verification
18
+
19
+ <!-- What you checked, and honestly what you didn't. -->
20
+ - [ ] Tests pass (`uv run pytest`)
21
+ -
22
+
23
+ ## Notes / follow-ups
24
+
25
+ <!-- Deferred work, risks, anything reviewers should know.
26
+ Link issues with "Closes #123" to auto-close them on merge. -->
27
+ -
@@ -0,0 +1,235 @@
1
+ # Builds the Windows app, macOS app, Android APK, and Python package. GUI
2
+ # artifacts go to one GitHub Release; stable Python packages also go to PyPI.
3
+ #
4
+ # It runs only when you push a version tag like "v0.1.0" — see how to do that
5
+ # in the README's "Cutting a release" section. The build jobs upload their
6
+ # artifacts; a final release job attaches them to one release and generates the
7
+ # notes once (so they aren't duplicated).
8
+
9
+ name: Release
10
+
11
+ on:
12
+ push:
13
+ tags:
14
+ - "v*" # any tag starting with v, e.g. v0.1.0, v1.2.3
15
+
16
+ # The automatic GITHUB_TOKEN needs write access to create the Release.
17
+ permissions:
18
+ contents: write
19
+
20
+ env:
21
+ PYTHONUTF8: "1" # make Python use UTF-8 everywhere (our progress text uses •, ↷, …)
22
+ FLET_CLI_NO_RICH_OUTPUT: "1" # plain, log-friendly output from `flet build`
23
+
24
+ jobs:
25
+ python-package:
26
+ # Pre-releases remain GitHub-only. `uvx intervalssync@latest` therefore
27
+ # always resolves to a tested stable release from PyPI.
28
+ if: ${{ !contains(github.ref_name, '-') }}
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: astral-sh/setup-uv@v6
33
+
34
+ - name: Compute version from tag
35
+ run: echo "APP_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
36
+
37
+ - name: Stamp package and runtime versions
38
+ run: |
39
+ uv version "$APP_VERSION" --frozen
40
+ echo "__version__ = \"$APP_VERSION\"" > src/intervalssync/_version.py
41
+
42
+ - name: Build distributions
43
+ run: uv build
44
+
45
+ - name: Validate distributions
46
+ run: uvx --from twine twine check dist/*
47
+
48
+ - name: Smoke-test the wheel
49
+ run: |
50
+ wheel="dist/intervalssync-${APP_VERSION}-py3-none-any.whl"
51
+ uvx --python 3.13 --from "$wheel" intervalssync --version
52
+ uvx --python 3.13 --from "$wheel" intervalssync --help
53
+ mkdir -p "$RUNNER_TEMP/hermes"
54
+ {
55
+ echo "INTERVALSSYNC_IGPSPORT_USER=test@example.com"
56
+ echo "INTERVALSSYNC_IGPSPORT_PASSWORD=test-password"
57
+ echo "INTERVALSSYNC_INTERVALS_API_KEY=test-api-key"
58
+ } > "$RUNNER_TEMP/hermes/.env"
59
+ HERMES_HOME="$RUNNER_TEMP/hermes" uvx --python 3.13 --from "$wheel" intervalssync check --json
60
+
61
+ - uses: actions/upload-artifact@v4
62
+ with:
63
+ name: python-package
64
+ path: dist/*
65
+
66
+ windows:
67
+ runs-on: windows-latest # ships with Visual Studio 2022 + C++
68
+ steps:
69
+ - uses: actions/checkout@v4
70
+ - uses: astral-sh/setup-uv@v6
71
+
72
+ # The git tag is the single source of truth for the version. Strip the
73
+ # leading "v" (v0.2.1 -> 0.2.1) for flet's x.y.z build-version.
74
+ - name: Compute version from tag
75
+ shell: bash
76
+ run: echo "APP_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
77
+
78
+ # Stamp it into the app so the in-app About dialog shows the real version.
79
+ - name: Stamp version into the app
80
+ shell: bash
81
+ run: echo "__version__ = \"$APP_VERSION\"" > src/intervalssync/_version.py
82
+
83
+ - name: Stamp Dropbox app key into the app
84
+ shell: pwsh
85
+ env:
86
+ IGPSYNC_DROPBOX_APP_KEY: ${{ secrets.IGPSYNC_DROPBOX_APP_KEY }}
87
+ run: |
88
+ python -c "from pathlib import Path; import os; Path('src/intervalssync/_dropbox_app_key.py').write_text('DROPBOX_APP_KEY = ' + repr(os.getenv('IGPSYNC_DROPBOX_APP_KEY', '')) + '\n', encoding='utf-8')"
89
+
90
+ - name: Build Windows app
91
+ run: uv run flet build windows --yes --verbose --build-version "${{ env.APP_VERSION }}"
92
+
93
+ # Name the zip after the tag, e.g. intervalssync-v0.2.0-windows.zip
94
+ - name: Package into a versioned zip
95
+ shell: pwsh
96
+ run: Compress-Archive -Path build/windows/* -DestinationPath "intervalssync-${{ github.ref_name }}-windows.zip"
97
+
98
+ - uses: actions/upload-artifact@v4
99
+ with:
100
+ name: windows
101
+ path: "intervalssync-${{ github.ref_name }}-windows.zip"
102
+
103
+ android:
104
+ runs-on: ubuntu-latest
105
+ steps:
106
+ - uses: actions/checkout@v4
107
+ - uses: astral-sh/setup-uv@v6
108
+
109
+ - name: Compute version from tag
110
+ run: echo "APP_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
111
+
112
+ - name: Stamp version into the app
113
+ run: echo "__version__ = \"$APP_VERSION\"" > src/intervalssync/_version.py
114
+
115
+ - name: Stamp Dropbox app key into the app
116
+ env:
117
+ IGPSYNC_DROPBOX_APP_KEY: ${{ secrets.IGPSYNC_DROPBOX_APP_KEY }}
118
+ run: |
119
+ python -c "from pathlib import Path; import os; Path('src/intervalssync/_dropbox_app_key.py').write_text('DROPBOX_APP_KEY = ' + repr(os.getenv('IGPSYNC_DROPBOX_APP_KEY', '')) + '\n', encoding='utf-8')"
120
+
121
+ # Use a stable release key if the keystore secrets are configured, so every
122
+ # release shares one signature and Android installs updates over the top
123
+ # (no uninstall). Without the secrets, flet falls back to a debug key.
124
+ - name: Configure APK signing (if keystore secret is set)
125
+ env:
126
+ KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
127
+ KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
128
+ KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
129
+ run: |
130
+ if [ -n "$KEYSTORE_B64" ]; then
131
+ echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/release.jks"
132
+ {
133
+ echo "FLET_ANDROID_SIGNING_KEY_STORE=$RUNNER_TEMP/release.jks"
134
+ echo "FLET_ANDROID_SIGNING_KEY_STORE_PASSWORD=$KEYSTORE_PASSWORD"
135
+ echo "FLET_ANDROID_SIGNING_KEY_PASSWORD=$KEYSTORE_PASSWORD"
136
+ echo "FLET_ANDROID_SIGNING_KEY_ALIAS=$KEY_ALIAS"
137
+ } >> "$GITHUB_ENV"
138
+ echo "Release signing configured."
139
+ else
140
+ echo "No keystore secret set — APK will be debug-signed."
141
+ fi
142
+
143
+ # flet build apk auto-installs the Flutter/Android SDK + JDK on first run.
144
+ # It picks up the FLET_ANDROID_SIGNING_* env vars set above when present.
145
+ - name: Build Android APK
146
+ run: uv run flet build apk --yes --verbose --build-version "${{ env.APP_VERSION }}"
147
+
148
+ - name: Rename APK with the version
149
+ run: mv build/apk/*.apk "intervalssync-${{ github.ref_name }}-android.apk"
150
+
151
+ - uses: actions/upload-artifact@v4
152
+ with:
153
+ name: android
154
+ path: "intervalssync-${{ github.ref_name }}-android.apk"
155
+
156
+ macos:
157
+ runs-on: macos-latest
158
+ steps:
159
+ - uses: actions/checkout@v4
160
+ - uses: astral-sh/setup-uv@v6
161
+
162
+ - name: Compute version from tag
163
+ run: echo "APP_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
164
+
165
+ - name: Stamp version into the app
166
+ run: echo "__version__ = \"$APP_VERSION\"" > src/intervalssync/_version.py
167
+
168
+ - name: Stamp Dropbox app key into the app
169
+ env:
170
+ IGPSYNC_DROPBOX_APP_KEY: ${{ secrets.IGPSYNC_DROPBOX_APP_KEY }}
171
+ run: |
172
+ python -c "from pathlib import Path; import os; Path('src/intervalssync/_dropbox_app_key.py').write_text('DROPBOX_APP_KEY = ' + repr(os.getenv('IGPSYNC_DROPBOX_APP_KEY', '')) + '\n', encoding='utf-8')"
173
+
174
+ - name: Build macOS app
175
+ run: uv run flet build macos --yes --verbose --build-version "${{ env.APP_VERSION }}"
176
+
177
+ - name: Package into a versioned zip
178
+ run: |
179
+ for app in build/macos/*.app; do
180
+ if [ -d "$app" ]; then
181
+ ditto -c -k --sequesterRsrc --keepParent "$app" "intervalssync-${{ github.ref_name }}-macos.zip"
182
+ exit 0
183
+ fi
184
+ done
185
+ echo "No macOS .app bundle found in build/macos" >&2
186
+ exit 1
187
+
188
+ - uses: actions/upload-artifact@v4
189
+ with:
190
+ name: macos
191
+ path: "intervalssync-${{ github.ref_name }}-macos.zip"
192
+
193
+ pypi-publish:
194
+ if: ${{ !contains(github.ref_name, '-') }}
195
+ needs: python-package
196
+ runs-on: ubuntu-latest
197
+ environment:
198
+ name: pypi
199
+ url: https://pypi.org/p/intervalssync
200
+ permissions:
201
+ contents: read
202
+ id-token: write
203
+ steps:
204
+ - name: Download Python distributions
205
+ uses: actions/download-artifact@v4
206
+ with:
207
+ name: python-package
208
+ path: dist
209
+
210
+ - name: Publish distributions to PyPI
211
+ uses: pypa/gh-action-pypi-publish@release/v1
212
+
213
+ release:
214
+ needs: [windows, android, macos]
215
+ runs-on: ubuntu-latest
216
+ steps:
217
+ - name: Download build artifacts
218
+ uses: actions/download-artifact@v4
219
+ with:
220
+ path: dist
221
+
222
+ # Create (or update) the Release for this tag, attach all files, and
223
+ # generate the notes a single time (avoids the duplicate notes you get
224
+ # when each build job generates them).
225
+ #
226
+ # Tags with a hyphen (e.g. v0.3.0-rc1) are marked as pre-releases: GitHub
227
+ # keeps the last stable as "Latest", the in-app update check (which uses
228
+ # the /releases/latest API) ignores them, and they aren't offered to users.
229
+ - name: Publish Release
230
+ uses: softprops/action-gh-release@v2
231
+ with:
232
+ files: dist/**/*
233
+ generate_release_notes: true
234
+ prerelease: ${{ contains(github.ref_name, '-') }}
235
+ make_latest: ${{ !contains(github.ref_name, '-') }}
@@ -0,0 +1,25 @@
1
+ # Runs the test suite on every push and pull request, so regressions (including
2
+ # from contributors' PRs) are caught before merging.
3
+
4
+ name: Tests
5
+
6
+ on:
7
+ push:
8
+ branches: ["master"]
9
+ pull_request:
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Check out code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Set up uv
19
+ uses: astral-sh/setup-uv@v6
20
+
21
+ - name: Install dependencies (incl. dev group)
22
+ run: uv sync
23
+
24
+ - name: Run tests
25
+ run: uv run pytest -q
@@ -0,0 +1,19 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .env
13
+
14
+ Downloads/
15
+ downloads/
16
+
17
+ # Flet build output
18
+ build/
19
+ .flet/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,67 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ **Intervals Sync** — a cross-platform app that syncs cycling activities to **intervals.icu** from **iGPSPORT** or **Bryton Active**, and can upload planned workouts from intervals.icu back to iGPSPORT. It ships as a **Flet** GUI (`intervalssync-gui`) plus a headless **CLI** (`intervalssync`). Dependency management uses [uv](https://docs.astral.dev/uv/) (Python 3.13).
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ uv sync
13
+ uv run intervalssync-gui
14
+ uv run intervalssync sync # iGPSPORT (default)
15
+ uv run intervalssync sync --source bryton # Bryton Active
16
+ uv run intervalssync upload-workouts --json # intervals.icu → iGPSPORT
17
+ uv run intervalssync sync-zones --env-file .env --json # intervals.icu → iGPSPORT profile
18
+ uv run intervalssync check --source bryton
19
+ uv run flet build windows
20
+ uv run pytest
21
+ ```
22
+
23
+ ## Tests
24
+
25
+ `pytest` under `tests/` — offline, no network. iGPSPORT tests in `test_igpsport_core.py`; Bryton in `test_bryton_core.py`. Config tests redirect `CONFIG_PATH` to a tmp dir.
26
+
27
+ ## Architecture
28
+
29
+ Package root: `src/intervalssync/`. **CLI** and **GUI** are separate subpackages; **activity sources** are sibling subpackages under the root.
30
+
31
+ ```
32
+ src/intervalssync/
33
+ cli/main.py, env.py, config.py # headless CLI
34
+ gui/app.py, views, config.py, secrets.py # Flet UI (dual-source: iGPSPORT + Bryton)
35
+ igpsport/core.py, workout.py # iGPSPORT source
36
+ bryton/ddp.py, api.py, core.py # Bryton source
37
+ intervals_icu.py, dropbox_client.py # shared by sources
38
+ ```
39
+
40
+ - **`intervals_icu.py`** — shared upload, skip-existing (`external_id`), sport PUT, calendar workout fetch, sport-settings lookup (`fetch_sport_settings`, `max_hr`).
41
+ - **`igpsport/core.py`** — `sync(SyncConfig, progress)`: login → list → FIT URL → download → upload. `external_id`: `igpsport_{ride_id}`.
42
+ - **`igpsport/profile_sync.py`** — `sync_profile_zones`: intervals.icu sport settings + athlete weight → iGPSPORT profile thresholds/zones/weight.
43
+ - **`igpsport/workout.py`** — planned workouts intervals.icu → iGPSPORT.
44
+ - **`bryton/ddp.py`** — Meteor DDP login + `activityList` subscription.
45
+ - **`bryton/api.py`** — `GET https://m3.brytonactive.com/api/activity?id=…` (FIT download; Android app API).
46
+ - **`bryton/core.py`** — `sync(SyncConfig, progress)`. `external_id`: `bryton_{activity_id}`.
47
+ - **`gui/secrets.py`** — async OS vault via `flet-secure-storage`.
48
+ - **`gui/config.py`** — GUI JSON settings: `enable_igpsport` / `enable_bryton`, per-source usernames, shared sync options.
49
+ - **`gui/sync_view.py`** — dispatches to `igpsport.core.sync` or `bryton.core.sync` per enabled source; workout upload is iGPSPORT-only.
50
+ - **`cli/env.py`** — `.env` credential loading for the headless CLI.
51
+
52
+ ## iGPSPORT API notes
53
+
54
+ 1. **International:** POST `i.igpsport.com/Auth/Login` → `loginToken` cookie URL-decoded as Bearer token.
55
+ 2. **China:** POST `prod.zh.igpsport.com/service/auth/account/login` with `{appId, username, password}` → JSON `data.access_token` as Bearer.
56
+ 3. `Activity/ActivityList` (intl) or `queryMyActivity` (China) — PascalCase / camelCase field names respectively.
57
+ 4. FIT URL: `{prod.en|zh}.igpsport.com/.../queryActivityDetail/{id}` or `getDownloadUrl/{id}`.
58
+
59
+ ## Bryton API notes
60
+
61
+ 1. Login: Meteor DDP `login` with email + SHA-256 password digest on `m3.brytonactive.com`.
62
+ 2. List: subscribe `activityList` → `userActivities` collection.
63
+ 3. FIT: REST `GET /api/activity?id={_id}` with `X-User-Id`, `X-Auth-Token`, static `x-api-key` (not the web `activity.detail.2` samples API).
64
+
65
+ ## Flet version note
66
+
67
+ Built against Flet 0.86.x. Uses `ft.Icons` / `ft.Colors`, `page.show_dialog`, `ft.run(main)`.
@@ -0,0 +1,156 @@
1
+ # Contributing
2
+
3
+ Thanks for your interest in improving **Intervals Sync**! This guide covers how to
4
+ set up the project, make a change, and open a pull request.
5
+
6
+ ## Prerequisites
7
+
8
+ - [uv](https://docs.astral.sh/uv/) (manages Python and dependencies)
9
+ - Python 3.13 (uv will install it for you from `.python-version`)
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ git clone https://github.com/jorge-huxley/intervalssync.git
15
+ cd intervalssync
16
+ uv sync # installs runtime + dev dependencies into .venv
17
+ ```
18
+
19
+ Run the app while developing:
20
+
21
+ ```bash
22
+ uv run intervalssync-gui # launch the GUI
23
+ ```
24
+
25
+ Headless sync and agent integration: [docs/AGENT.md](docs/AGENT.md).
26
+
27
+ ## Running the tests
28
+
29
+ ```bash
30
+ uv run pytest
31
+ ```
32
+
33
+ Tests are **offline** — they fake the network and never use real credentials, so
34
+ they're safe to run anywhere. Please add or update tests for any behavior you
35
+ change; the same suite runs in CI on every pull request and must pass before a
36
+ change can be merged.
37
+
38
+ ## Project layout
39
+
40
+ See `CLAUDE.md` for the full picture:
41
+
42
+ - `src/intervalssync/` — package root (CLI, GUI, config, shared `intervals_icu.py`)
43
+ - `src/intervalssync/igpsport/` — iGPSPORT activity sync + workout upload
44
+ - `src/intervalssync/bryton/` — Bryton Active activity sync
45
+ - `src/intervalssync/gui/` — Flet UI (`intervalssync-gui` entry)
46
+
47
+ Keep source logic testable and UI-free; CLI/GUI stay thin orchestration.
48
+
49
+ ## Working on the Dropbox upload (optional)
50
+
51
+ Dropbox is an optional, off-by-default upload target in the **GUI** (iGPSPORT
52
+ and Bryton sync). The headless CLI uploads to intervals.icu only. You only need
53
+ to set this up if you're working on that feature — without a key the Dropbox
54
+ switch in Settings stays disabled and the rest of the app works normally.
55
+
56
+ The app authenticates with Dropbox using the **PKCE** OAuth flow, which needs a
57
+ Dropbox **app key**. Releases get the key stamped in by CI from a repository
58
+ secret. For local development, **create your own Dropbox app** rather than
59
+ reusing the production one:
60
+
61
+ 1. In the [Dropbox App Console](https://www.dropbox.com/developers/apps), create
62
+ an app with **Scoped access** and **App folder** access, and enable the
63
+ `account_info.read`, `files.metadata.read`, and `files.content.write` scopes.
64
+ 2. Copy the app's **App key**.
65
+ 3. Copy `.env.example` to `.env`, set `IGPSYNC_DROPBOX_APP_KEY` to your key, and
66
+ run with uv's `--env-file` flag (which loads the file into the environment):
67
+
68
+ ```bash
69
+ cp .env.example .env # then edit .env and paste your app key
70
+ uv run --env-file .env intervalssync-gui
71
+ ```
72
+
73
+ `.env` is gitignored. The app key for a PKCE public client isn't a secret (it
74
+ ships inside released builds), but use your own for development so you don't
75
+ share the production app's rate limit.
76
+
77
+ ## Making a change
78
+
79
+ 1. **Fork** the repository (or create a branch if you're a collaborator).
80
+ 2. Create a topic branch: `git switch -c feat/short-description`.
81
+ 3. Make your change and add tests.
82
+ 4. Run `uv run pytest` and make sure everything passes.
83
+ 5. Commit using **[Conventional Commits](https://www.conventionalcommits.org/)**,
84
+ e.g. `feat(gui): add gravel ride to type list` or `fix(core): handle empty
85
+ activity list`. Common types: `feat`, `fix`, `docs`, `test`, `refactor`,
86
+ `chore`, `ci`.
87
+ 6. Push your branch and **open a pull request** against `master`. Describe what
88
+ changed and why.
89
+
90
+ ## Things to keep in mind
91
+
92
+ - **Never commit secrets.** Credentials live in the OS-native credential vault
93
+ (Windows Credential Manager / macOS Keychain / Android Keystore / Linux
94
+ libsecret) via the `flet-secure-storage` service — never in a file. Don't add
95
+ real API keys, passwords, or personal data to the repo, tests, or commits.
96
+ - The Flet API changes between versions — verify control arguments against the
97
+ installed version (currently 0.86.x) rather than older tutorials.
98
+
99
+ ## Cutting a release
100
+
101
+ Releases are built automatically by GitHub Actions (`.github/workflows/release.yml`)
102
+ whenever you push a version tag. To publish a new version:
103
+
104
+ ```bash
105
+ git tag v0.1.0 # pick the next version number
106
+ git push origin v0.1.0
107
+ ```
108
+
109
+ The workflow builds the Windows app, macOS app, Android APK, and Python
110
+ distributions on clean runners. It attaches the GUI artifacts to a new GitHub
111
+ Release and publishes stable Python distributions to PyPI, which makes the new
112
+ version available through `uvx intervalssync@latest`. Watch the workflow under
113
+ the repository's **Actions** tab.
114
+
115
+ PyPI publishing uses Trusted Publishing rather than an API-token secret. The
116
+ repository owner must configure a PyPI publisher for this repository with:
117
+
118
+ - owner: `jorge-huxley`
119
+ - repository: `intervalssync`
120
+ - workflow name: `release.yml` (PyPI wants only the filename, not the
121
+ `.github/workflows/` path)
122
+ - environment: `pypi`
123
+ - project: `intervalssync`
124
+
125
+ Create the matching protected `pypi` environment in GitHub before the first
126
+ stable package release. Existing PyPI versions are immutable, so retry a failed
127
+ release with a new version after fixing the workflow.
128
+
129
+ ### Pre-releases (test a build before shipping)
130
+
131
+ Tag with a hyphen, e.g. `v0.3.0-rc1`, to publish a **pre-release**. It still
132
+ builds installable artifacts you can test on a device, but GitHub keeps the last
133
+ stable as **Latest** and the in-app update check ignores it — so users on the
134
+ stable version aren't notified. Pre-releases are not published to PyPI. Once
135
+ it's good, tag the final `v0.3.0`.
136
+
137
+ ### Urgent hotfix while `master` has unreleased work
138
+
139
+ `master` only ever contains finished, merged PRs, so usually you can just merge
140
+ the fix and tag a patch. If `master` already has work you're not ready to ship,
141
+ branch the fix from the **last released tag** instead, so only the fix goes out:
142
+
143
+ ```bash
144
+ # 1. Branch from the last released tag (NOT master):
145
+ git switch -c hotfix/v0.2.5 v0.2.4
146
+ # 2. Commit the fix on this branch, then push it:
147
+ git push -u origin hotfix/v0.2.5
148
+ # 3. Tag this branch's fix commit -> builds & publishes only the fix:
149
+ git tag v0.2.5 && git push origin v0.2.5
150
+ # 4. Open a PR from hotfix/v0.2.5 into master and merge it, so the fix is
151
+ # also in master for future work (master is protected, so use a PR).
152
+ ```
153
+
154
+ The tag points at the hotfix commit, so the build contains just the fix — none
155
+ of master's unreleased work. The merge into master (step 4) is separate and
156
+ happens *after* tagging.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jorge Silva
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.