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.
- intervalssync-0.9.3/.env.example +19 -0
- intervalssync-0.9.3/.github/FUNDING.yml +1 -0
- intervalssync-0.9.3/.github/pull_request_template.md +27 -0
- intervalssync-0.9.3/.github/workflows/release.yml +235 -0
- intervalssync-0.9.3/.github/workflows/test.yml +25 -0
- intervalssync-0.9.3/.gitignore +19 -0
- intervalssync-0.9.3/.python-version +1 -0
- intervalssync-0.9.3/CLAUDE.md +67 -0
- intervalssync-0.9.3/CONTRIBUTING.md +156 -0
- intervalssync-0.9.3/LICENSE +21 -0
- intervalssync-0.9.3/PKG-INFO +179 -0
- intervalssync-0.9.3/README.md +158 -0
- intervalssync-0.9.3/assets/icon.png +0 -0
- intervalssync-0.9.3/docs/AGENT.md +191 -0
- intervalssync-0.9.3/docs/img/demo.gif +0 -0
- intervalssync-0.9.3/docs/img/settings.png +0 -0
- intervalssync-0.9.3/docs/img/sync.png +0 -0
- intervalssync-0.9.3/main.py +21 -0
- intervalssync-0.9.3/pyproject.toml +66 -0
- intervalssync-0.9.3/src/intervalssync/__init__.py +3 -0
- intervalssync-0.9.3/src/intervalssync/_dropbox_app_key.py +7 -0
- intervalssync-0.9.3/src/intervalssync/_version.py +1 -0
- intervalssync-0.9.3/src/intervalssync/bryton/__init__.py +1 -0
- intervalssync-0.9.3/src/intervalssync/bryton/api.py +49 -0
- intervalssync-0.9.3/src/intervalssync/bryton/core.py +249 -0
- intervalssync-0.9.3/src/intervalssync/bryton/ddp.py +231 -0
- intervalssync-0.9.3/src/intervalssync/bryton/exceptions.py +13 -0
- intervalssync-0.9.3/src/intervalssync/bryton/fit_encode.py +363 -0
- intervalssync-0.9.3/src/intervalssync/bryton/workout.py +342 -0
- intervalssync-0.9.3/src/intervalssync/cli/__init__.py +5 -0
- intervalssync-0.9.3/src/intervalssync/cli/config.py +53 -0
- intervalssync-0.9.3/src/intervalssync/cli/env.py +189 -0
- intervalssync-0.9.3/src/intervalssync/cli/main.py +637 -0
- intervalssync-0.9.3/src/intervalssync/dropbox_client.py +143 -0
- intervalssync-0.9.3/src/intervalssync/gui/__init__.py +1 -0
- intervalssync-0.9.3/src/intervalssync/gui/app.py +352 -0
- intervalssync-0.9.3/src/intervalssync/gui/config.py +102 -0
- intervalssync-0.9.3/src/intervalssync/gui/profile_sync_ui.py +225 -0
- intervalssync-0.9.3/src/intervalssync/gui/secrets.py +135 -0
- intervalssync-0.9.3/src/intervalssync/gui/settings_view.py +716 -0
- intervalssync-0.9.3/src/intervalssync/gui/support_gamification.py +658 -0
- intervalssync-0.9.3/src/intervalssync/gui/sync_view.py +486 -0
- intervalssync-0.9.3/src/intervalssync/gui/system.py +21 -0
- intervalssync-0.9.3/src/intervalssync/gui/theme.py +216 -0
- intervalssync-0.9.3/src/intervalssync/gui/update_check.py +55 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/__init__.py +1 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/core.py +520 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/interval_info.py +269 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/profile_sync.py +391 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/region.py +80 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/workout.py +503 -0
- intervalssync-0.9.3/src/intervalssync/igpsport/zone_map.py +143 -0
- intervalssync-0.9.3/src/intervalssync/intervals_icu.py +201 -0
- intervalssync-0.9.3/tests/fixtures/bryton_workouts/Foundation_2.fit +0 -0
- intervalssync-0.9.3/tests/fixtures/bryton_workouts/Foundation_2.json +94 -0
- intervalssync-0.9.3/tests/fixtures/bryton_workouts/app.fit +0 -0
- intervalssync-0.9.3/tests/fixtures/bryton_workouts/app.json +101 -0
- intervalssync-0.9.3/tests/fixtures/bryton_workouts/web_bsWO0618_011758.fit +0 -0
- intervalssync-0.9.3/tests/fixtures/bryton_workouts/web_bsWO0618_011758.json +112 -0
- intervalssync-0.9.3/tests/test_bryton_core.py +313 -0
- intervalssync-0.9.3/tests/test_bryton_fit_encode.py +291 -0
- intervalssync-0.9.3/tests/test_bryton_fit_native.py +52 -0
- intervalssync-0.9.3/tests/test_bryton_workout.py +287 -0
- intervalssync-0.9.3/tests/test_cli.py +390 -0
- intervalssync-0.9.3/tests/test_config.py +172 -0
- intervalssync-0.9.3/tests/test_dropbox_client.py +120 -0
- intervalssync-0.9.3/tests/test_gui_app.py +35 -0
- intervalssync-0.9.3/tests/test_igpsport_core.py +608 -0
- intervalssync-0.9.3/tests/test_intervals_icu.py +119 -0
- intervalssync-0.9.3/tests/test_profile_sync.py +380 -0
- intervalssync-0.9.3/tests/test_secrets.py +147 -0
- intervalssync-0.9.3/tests/test_support_gamification.py +92 -0
- intervalssync-0.9.3/tests/test_update_check.py +61 -0
- intervalssync-0.9.3/tests/test_workout.py +400 -0
- intervalssync-0.9.3/tests/test_zone_map.py +99 -0
- 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 @@
|
|
|
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.
|