eticu 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eticu-0.2.1/.env.example +2 -0
- eticu-0.2.1/.github/workflows/publish.yml +42 -0
- eticu-0.2.1/.gitignore +17 -0
- eticu-0.2.1/.python-version +1 -0
- eticu-0.2.1/CHANGELOG.md +41 -0
- eticu-0.2.1/CLAUDE.md +99 -0
- eticu-0.2.1/Makefile +30 -0
- eticu-0.2.1/PKG-INFO +109 -0
- eticu-0.2.1/README.md +93 -0
- eticu-0.2.1/docs/ISSUES.md +74 -0
- eticu-0.2.1/docs/ROADMAP.md +315 -0
- eticu-0.2.1/plans/sample_plan_olympic_l0.csv +7 -0
- eticu-0.2.1/pyproject.toml +59 -0
- eticu-0.2.1/src/eticu/__init__.py +1 -0
- eticu-0.2.1/src/eticu/cli.py +229 -0
- eticu-0.2.1/src/eticu/config.py +24 -0
- eticu-0.2.1/src/eticu/convert.py +130 -0
- eticu-0.2.1/src/eticu/download.py +115 -0
- eticu-0.2.1/src/eticu/fit_parse.py +286 -0
- eticu-0.2.1/src/eticu/intervals_client.py +224 -0
- eticu-0.2.1/src/eticu/models.py +28 -0
- eticu-0.2.1/src/eticu/plan.py +69 -0
- eticu-0.2.1/src/eticu/scrape.py +90 -0
- eticu-0.2.1/src/eticu/upload.py +173 -0
- eticu-0.2.1/src/eticu/zones.py +38 -0
- eticu-0.2.1/tests/__init__.py +0 -0
- eticu-0.2.1/tests/download_fixtures.py +57 -0
- eticu-0.2.1/tests/fixtures/CCI1.FIT +0 -0
- eticu-0.2.1/tests/test_convert.py +191 -0
- eticu-0.2.1/tests/test_fit_parse.py +164 -0
- eticu-0.2.1/tests/test_intervals_client.py +100 -0
- eticu-0.2.1/tests/test_zones.py +57 -0
- eticu-0.2.1/uv.lock +655 -0
eticu-0.2.1/.env.example
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Semantic Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write # Needed to push tags and releases to GitHub
|
|
10
|
+
id-token: write # Needed for trusted publishing to PyPI
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment:
|
|
16
|
+
name: pypi
|
|
17
|
+
url: https://pypi.org/p/eticu
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout repository
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v4
|
|
26
|
+
with:
|
|
27
|
+
version: "latest"
|
|
28
|
+
|
|
29
|
+
- name: Python Semantic Release
|
|
30
|
+
id: release
|
|
31
|
+
uses: python-semantic-release/python-semantic-release@v9.8.1
|
|
32
|
+
with:
|
|
33
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
34
|
+
|
|
35
|
+
- name: Build package
|
|
36
|
+
if: steps.release.outputs.released == 'true'
|
|
37
|
+
run: uv build
|
|
38
|
+
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
# Only publish if a new release was created
|
|
41
|
+
if: steps.release.outputs.released == 'true'
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
eticu-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
eticu-0.2.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## v0.2.1 (2026-07-06)
|
|
6
|
+
|
|
7
|
+
### Ci
|
|
8
|
+
|
|
9
|
+
* ci: add explicit build step after semantic release ([`5370e5a`](https://github.com/melihakay/8020-Fetcher/commit/5370e5ac0598451312efc36fc099bf7c151fed22))
|
|
10
|
+
|
|
11
|
+
### Fix
|
|
12
|
+
|
|
13
|
+
* fix: trigger publisher ([`24eff65`](https://github.com/melihakay/8020-Fetcher/commit/24eff653c359e75ebd26d20c7a7a02791e6dd233))
|
|
14
|
+
|
|
15
|
+
### Unknown
|
|
16
|
+
|
|
17
|
+
* Merge branch 'master' of https://github.com/melihakay/8020-Fetcher ([`8df2000`](https://github.com/melihakay/8020-Fetcher/commit/8df20006cce8f0a169dfd3870463e7ad4e08a03e))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## v0.2.0 (2026-07-06)
|
|
21
|
+
|
|
22
|
+
### Feature
|
|
23
|
+
|
|
24
|
+
* feat: added semantic versioning ([`d42e355`](https://github.com/melihakay/8020-Fetcher/commit/d42e355e822b9d7180aaaa4d4cfa16a5c74d43c1))
|
|
25
|
+
|
|
26
|
+
### Fix
|
|
27
|
+
|
|
28
|
+
* fix: use default semantic-release build command ([`04315fc`](https://github.com/melihakay/8020-Fetcher/commit/04315fcbc5b133f5d4c2a4e5bc1a451ced3d0262))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## v0.1.0 (2026-07-06)
|
|
32
|
+
|
|
33
|
+
### Feature
|
|
34
|
+
|
|
35
|
+
* feat: added dynamic versioning and publisher ([`14573ca`](https://github.com/melihakay/8020-Fetcher/commit/14573ca967a4faf057e863dd078b679412746c02))
|
|
36
|
+
|
|
37
|
+
### Unknown
|
|
38
|
+
|
|
39
|
+
* update readme ([`0c6f3a7`](https://github.com/melihakay/8020-Fetcher/commit/0c6f3a7e2a1c1f2eac406ac98eba9ec2b614ae96))
|
|
40
|
+
|
|
41
|
+
* initial version ([`68e92ec`](https://github.com/melihakay/8020-Fetcher/commit/68e92ec95c07469b0497d8dbeaa2076815e08c9d))
|
eticu-0.2.1/CLAUDE.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Operational guide for working in this repo. **`ROADMAP.md` is the authoritative spec** — read it
|
|
4
|
+
first, especially §0 (the domain gotchas). This file is the how-to-work-here layer: commands,
|
|
5
|
+
conventions, and the short list of things that will bite you.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
A Python package (`eticu`, CLI `eticu`) that scrapes the **free** 80/20 Endurance
|
|
10
|
+
workout library, converts each `.FIT` workout into a correct intervals.icu structured workout,
|
|
11
|
+
and uploads them into the intervals.icu workout library. Metric units only. Swims 25 m / 50 m
|
|
12
|
+
only. Reimplements and fixes `njg4ne/8020-intervals-icu` (TS, GPL-3.0).
|
|
13
|
+
|
|
14
|
+
Only ever use the **free public library**. Never scrape, embed, or redistribute paid 80/20
|
|
15
|
+
training plans.
|
|
16
|
+
|
|
17
|
+
## Stack & tooling
|
|
18
|
+
|
|
19
|
+
- Python **3.12+**, **uv** for env/deps, `src/` layout.
|
|
20
|
+
- `ruff` (lint + format), `mypy --strict`, `pytest` (+ `respx` for HTTP mocking).
|
|
21
|
+
- Runtime deps (see ROADMAP §1): a FIT decoder (`garmin-fit-sdk` or `fitdecode`), `httpx`,
|
|
22
|
+
`selectolax`/`beautifulsoup4`, `typer`, `pydantic` + `pydantic-settings`.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv sync # install (creates .venv, resolves lockfile)
|
|
28
|
+
uv run eticu --help # CLI
|
|
29
|
+
uv run eticu all --dry-run # full pipeline, no network writes
|
|
30
|
+
uv run eticu all --cycling-power # rides as Power targets; run/swim stay HR
|
|
31
|
+
|
|
32
|
+
uv run pytest -q # tests
|
|
33
|
+
uv run ruff check . # lint
|
|
34
|
+
uv run ruff format . # format
|
|
35
|
+
uv run mypy src # typecheck
|
|
36
|
+
uv build # build wheel/sdist
|
|
37
|
+
|
|
38
|
+
uv add <pkg> # add runtime dep
|
|
39
|
+
uv add --dev <pkg> # add dev dep
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Run `ruff check`, `ruff format`, `mypy src`, and `pytest` before considering any change done.
|
|
43
|
+
|
|
44
|
+
## Layout
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
src/eticu/ models.py zones.py scrape.py download.py
|
|
48
|
+
fit_parse.py convert.py intervals_client.py upload.py cli.py
|
|
49
|
+
tests/ unit tests + fixtures/ (vendored sample .FIT + golden JSON/text)
|
|
50
|
+
ROADMAP.md the spec (phases, acceptance criteria, correctness checklist)
|
|
51
|
+
CLAUDE.md this file
|
|
52
|
+
.env.example INTERVALS_API_KEY=, INTERVALS_ATHLETE_ID=
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Conventions
|
|
56
|
+
|
|
57
|
+
- Full type hints everywhere; `mypy --strict` must pass. Prefer pure functions in `zones`,
|
|
58
|
+
`fit_parse`, and `convert` so they're trivially testable.
|
|
59
|
+
- Keep the `Workout`/`Step` model **unit-agnostic** (store seconds and meters). All unit and
|
|
60
|
+
target-syntax formatting happens in `convert.py`, never in the parser.
|
|
61
|
+
- Golden-file testing is the backbone. Fixtures live in `tests/fixtures/`. **CCI1 is the
|
|
62
|
+
canonical regression case** (ROADMAP §0.5) — it guards both the target-loss fix and the
|
|
63
|
+
repeat-scoping fix, and it must pass in **both** HR mode and `--cycling-power` mode.
|
|
64
|
+
- **No live network calls in tests.** Mock intervals.icu with `respx`; use vendored `.FIT`
|
|
65
|
+
fixtures rather than hitting 8020's site. Never commit an API key or real athlete ID.
|
|
66
|
+
- Conventional Commits; small, phase-aligned PRs that each leave the suite green.
|
|
67
|
+
|
|
68
|
+
## Domain gotchas (full detail in ROADMAP §0 — do not re-derive)
|
|
69
|
+
|
|
70
|
+
- **Targets aren't in the FIT.** Steps use `target_type = open`; the zone is in the `notes`
|
|
71
|
+
string. Reading the file's target field gives you Z1 for everything — that's the bug. (§0.1)
|
|
72
|
+
- **Zone map inserts X and Y:** 80/20 `1,2,X,3,Y,4,5` → intervals.icu `Z1..Z7`, so **80/20
|
|
73
|
+
Zone 3 → Z4**. Assert this. (§0.2)
|
|
74
|
+
- **Repeats scope to the loop-back index only.** The repeat step names the message index to
|
|
75
|
+
return to; the `Nx` block wraps exactly that range up to the repeat instruction — steps after
|
|
76
|
+
it are siblings, not children. (§0.5)
|
|
77
|
+
- **Metric only.** Run/Ride distance → km; swim → meters (25 m increments). No `mi`, no `y`. (§0.6)
|
|
78
|
+
- **Swim 25 m / 50 m only**, never 25 y; each yields a name-suffixed workout. (§0.7)
|
|
79
|
+
- **Target type by sport:** Run/Swim always HR; Ride HR by default, **Power** under
|
|
80
|
+
`--cycling-power` (needs the athlete's FTP/Power zones; the Zwift/ERG use case). Same
|
|
81
|
+
Z-number mapping for both. (§0.3)
|
|
82
|
+
- **Folders are flat and fixed-named:** `80/20 - Run`, `80/20 - Bike`, `80/20 - Swim`
|
|
83
|
+
(intervals.icu doesn't nest folders; note "Bike" label vs `Ride` type). (§0.8)
|
|
84
|
+
- **Idempotent uploads:** re-running must not duplicate folders or workouts — look up by name,
|
|
85
|
+
then update or skip. (Phase 4)
|
|
86
|
+
|
|
87
|
+
## Config & secrets
|
|
88
|
+
|
|
89
|
+
Read `INTERVALS_API_KEY` and `INTERVALS_ATHLETE_ID` from the environment via
|
|
90
|
+
`pydantic-settings` (`.env` locally, never committed — ship `.env.example`). Auth is HTTP Basic:
|
|
91
|
+
username literal `API_KEY`, password = the key; athlete ID goes in the path
|
|
92
|
+
(`https://intervals.icu/api/v1/athlete/{id}/...`). Verify exact endpoints and the bulk-upsert
|
|
93
|
+
semantics against `https://intervals.icu/api-docs.html` before relying on them.
|
|
94
|
+
|
|
95
|
+
## Etiquette
|
|
96
|
+
|
|
97
|
+
Be a good citizen of both services: resumable downloads (skip existing, atomic writes), modest
|
|
98
|
+
concurrency and a small delay when scraping 8020, and idempotent writes to intervals.icu. Prefer
|
|
99
|
+
a `--dry-run` path for anything that mutates the remote library.
|
eticu-0.2.1/Makefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.PHONY: install format lint test typecheck build publish clean
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync
|
|
5
|
+
|
|
6
|
+
format:
|
|
7
|
+
uv run ruff format .
|
|
8
|
+
|
|
9
|
+
lint:
|
|
10
|
+
uv run ruff check .
|
|
11
|
+
|
|
12
|
+
typecheck:
|
|
13
|
+
uv run mypy src
|
|
14
|
+
|
|
15
|
+
test:
|
|
16
|
+
uv run pytest -q
|
|
17
|
+
|
|
18
|
+
build:
|
|
19
|
+
uv build
|
|
20
|
+
|
|
21
|
+
publish: build
|
|
22
|
+
uv publish
|
|
23
|
+
|
|
24
|
+
clean:
|
|
25
|
+
rm -rf dist/
|
|
26
|
+
rm -rf build/
|
|
27
|
+
rm -rf .ruff_cache/
|
|
28
|
+
rm -rf .pytest_cache/
|
|
29
|
+
rm -rf .mypy_cache/
|
|
30
|
+
find . -type d -name __pycache__ -exec rm -rf {} +
|
eticu-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eticu
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Convert 80/20 Endurance .FIT workouts to intervals.icu structured workouts
|
|
5
|
+
Project-URL: Repository, https://github.com/melihakay/8020-fetcher
|
|
6
|
+
Author-email: Melih Akay <melih.akay@metu.edu.tr>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: fitdecode>=0.10.0
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.3.0
|
|
12
|
+
Requires-Dist: pydantic>=2.7.0
|
|
13
|
+
Requires-Dist: selectolax>=0.3.17
|
|
14
|
+
Requires-Dist: typer>=0.12.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# eticu: 80/20 Endurance to Intervals.icu
|
|
18
|
+
|
|
19
|
+
`eticu` is a Python CLI pipeline that scrapes the **free** 80/20 Endurance workout library, parses and converts the `.FIT` files into fully structured [Intervals.icu](https://intervals.icu/) workouts, and idempotently uploads them to your Intervals.icu workout library.
|
|
20
|
+
|
|
21
|
+
It serves as a more robust, metric-first Python reimplementation of the [8020-intervals-icu](https://github.com/njg4ne/8020-intervals-icu) TypeScript project, completely fixing target loss issues during imports, nested repeat structures, and adding idempotent uploading.
|
|
22
|
+
|
|
23
|
+
> **Scope Note**: Brick (`BR*`) workouts do not exist as `.FIT` files in the library and are therefore also out of scope.
|
|
24
|
+
|
|
25
|
+
## Installation & Setup
|
|
26
|
+
|
|
27
|
+
This project uses `uv` for dependency management.
|
|
28
|
+
|
|
29
|
+
1. **Clone the repository:**
|
|
30
|
+
```bash
|
|
31
|
+
git clone <repo-url>
|
|
32
|
+
cd 8020-fetcher
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. **Environment Variables:**
|
|
36
|
+
Copy `.env.example` to `.env` and set your Intervals.icu credentials:
|
|
37
|
+
```env
|
|
38
|
+
INTERVALS_API_KEY=your_intervals_api_key
|
|
39
|
+
INTERVALS_ATHLETE_ID=your_intervals_athlete_id
|
|
40
|
+
```
|
|
41
|
+
You can find these in your Intervals.icu settings.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
To run the full pipeline (scrape → download → upload):
|
|
46
|
+
```bash
|
|
47
|
+
uv run eticu all
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Options:**
|
|
51
|
+
- `--dry-run`: Do everything except network writes to Intervals.icu.
|
|
52
|
+
- `--cycling-power`: Emit Power targets (instead of Heart Rate) for Ride workouts. (Run and Swim always remain HR).
|
|
53
|
+
- `--cache-dir <dir>`: Directory to cache downloaded `.FIT` files (defaults to `8020_cache`).
|
|
54
|
+
|
|
55
|
+
To convert and view a single downloaded `.FIT` file locally:
|
|
56
|
+
```bash
|
|
57
|
+
uv run eticu convert 8020_cache/bike/CCI1.FIT
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To upload a training plan from a CSV file (e.g. `plans/olympic_distance_level_0.csv`):
|
|
61
|
+
```bash
|
|
62
|
+
uv run eticu upload-plan plans/olympic_distance_level_0.csv --name "Olympic Level 0"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Exercise names in the CSV's should be same as the 80/20 FIT files.
|
|
66
|
+
|
|
67
|
+
**Options:**
|
|
68
|
+
- `--pool-length`: Pool length in meters (e.g., 25 or 50) for swim workouts.
|
|
69
|
+
- `--dry-run`: Parse the CSV and list the plan without uploading.
|
|
70
|
+
|
|
71
|
+
## Critical Prerequisites
|
|
72
|
+
|
|
73
|
+
For the generated workouts to be accurate, you **must** configure your Intervals.icu settings to match the 80/20 Endurance methodology.
|
|
74
|
+
|
|
75
|
+
### 1. The 7-Zone Prerequisite
|
|
76
|
+
80/20 uses a 7-level zone system with two crossover zones: `1, 2, X, 3, Y, 4, 5`.
|
|
77
|
+
Intervals.icu supports 7 zones (`Z1` to `Z7`). `eticu` maps them sequentially.
|
|
78
|
+
**You MUST update your Intervals.icu HR zones (and Power zones, if using `--cycling-power`) to match the numbers from the 80/20 zone calculator.**
|
|
79
|
+
|
|
80
|
+
| 80/20 Zone | Intervals.icu Zone |
|
|
81
|
+
|------------|--------------------|
|
|
82
|
+
| Zone 1 | Z1 |
|
|
83
|
+
| Zone 2 | Z2 |
|
|
84
|
+
| Zone X | Z3 |
|
|
85
|
+
| **Zone 3** | **Z4** |
|
|
86
|
+
| Zone Y | Z5 |
|
|
87
|
+
| Zone 4 | Z6 |
|
|
88
|
+
| Zone 5 | Z7 |
|
|
89
|
+
|
|
90
|
+
*Note: Because of crossover Zone X, 80/20's "Zone 3" maps to Intervals "Z4".*
|
|
91
|
+
|
|
92
|
+
### 2. The `--cycling-power` Option & ERG Mode
|
|
93
|
+
By default, 80/20 workouts use Heart Rate targets. However, platforms like **Zwift** in ERG mode ignore HR targets; they require **Power (%FTP)**.
|
|
94
|
+
|
|
95
|
+
If you ride on Zwift/TrainerRoad, pass the `--cycling-power` flag. This converts all `Ride` workouts to use Power zones.
|
|
96
|
+
* **Prerequisite**: For this to work, you MUST have your FTP and 7-level Power zones correctly configured in your Intervals.icu settings. If you don't, power targets will be meaningless.
|
|
97
|
+
|
|
98
|
+
## Core Behaviors
|
|
99
|
+
|
|
100
|
+
- **Metric Units**: All outputs are metric. Run and Ride distances are rounded to the nearest `0.1 km`. Swims are strictly handled in `meters` (`mtr` in Intervals.icu to distinguish from minutes).
|
|
101
|
+
- **Pool Lengths**: Only `25m` and `50m` swim workouts are fetched and processed. `25y` files are skipped.
|
|
102
|
+
- **Folder Layout**: Workouts are uploaded into three flat folders (created automatically if they don't exist):
|
|
103
|
+
- `80/20 - Run`
|
|
104
|
+
- `80/20 - Bike`
|
|
105
|
+
- `80/20 - Swim`
|
|
106
|
+
- **Idempotency**: The upload is completely idempotent. It matches workouts by exact name within the folder and skips identical workouts, preventing duplication if the script is run multiple times.
|
|
107
|
+
|
|
108
|
+
## Credits & License
|
|
109
|
+
This project was heavily inspired by the GPL-3.0 licensed `njg4ne/8020-intervals-icu` project, expanding its capabilities into a fully-typed Python package with deeper FIT file introspection.
|
eticu-0.2.1/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# eticu: 80/20 Endurance to Intervals.icu
|
|
2
|
+
|
|
3
|
+
`eticu` is a Python CLI pipeline that scrapes the **free** 80/20 Endurance workout library, parses and converts the `.FIT` files into fully structured [Intervals.icu](https://intervals.icu/) workouts, and idempotently uploads them to your Intervals.icu workout library.
|
|
4
|
+
|
|
5
|
+
It serves as a more robust, metric-first Python reimplementation of the [8020-intervals-icu](https://github.com/njg4ne/8020-intervals-icu) TypeScript project, completely fixing target loss issues during imports, nested repeat structures, and adding idempotent uploading.
|
|
6
|
+
|
|
7
|
+
> **Scope Note**: Brick (`BR*`) workouts do not exist as `.FIT` files in the library and are therefore also out of scope.
|
|
8
|
+
|
|
9
|
+
## Installation & Setup
|
|
10
|
+
|
|
11
|
+
This project uses `uv` for dependency management.
|
|
12
|
+
|
|
13
|
+
1. **Clone the repository:**
|
|
14
|
+
```bash
|
|
15
|
+
git clone <repo-url>
|
|
16
|
+
cd 8020-fetcher
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. **Environment Variables:**
|
|
20
|
+
Copy `.env.example` to `.env` and set your Intervals.icu credentials:
|
|
21
|
+
```env
|
|
22
|
+
INTERVALS_API_KEY=your_intervals_api_key
|
|
23
|
+
INTERVALS_ATHLETE_ID=your_intervals_athlete_id
|
|
24
|
+
```
|
|
25
|
+
You can find these in your Intervals.icu settings.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
To run the full pipeline (scrape → download → upload):
|
|
30
|
+
```bash
|
|
31
|
+
uv run eticu all
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Options:**
|
|
35
|
+
- `--dry-run`: Do everything except network writes to Intervals.icu.
|
|
36
|
+
- `--cycling-power`: Emit Power targets (instead of Heart Rate) for Ride workouts. (Run and Swim always remain HR).
|
|
37
|
+
- `--cache-dir <dir>`: Directory to cache downloaded `.FIT` files (defaults to `8020_cache`).
|
|
38
|
+
|
|
39
|
+
To convert and view a single downloaded `.FIT` file locally:
|
|
40
|
+
```bash
|
|
41
|
+
uv run eticu convert 8020_cache/bike/CCI1.FIT
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
To upload a training plan from a CSV file (e.g. `plans/olympic_distance_level_0.csv`):
|
|
45
|
+
```bash
|
|
46
|
+
uv run eticu upload-plan plans/olympic_distance_level_0.csv --name "Olympic Level 0"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Exercise names in the CSV's should be same as the 80/20 FIT files.
|
|
50
|
+
|
|
51
|
+
**Options:**
|
|
52
|
+
- `--pool-length`: Pool length in meters (e.g., 25 or 50) for swim workouts.
|
|
53
|
+
- `--dry-run`: Parse the CSV and list the plan without uploading.
|
|
54
|
+
|
|
55
|
+
## Critical Prerequisites
|
|
56
|
+
|
|
57
|
+
For the generated workouts to be accurate, you **must** configure your Intervals.icu settings to match the 80/20 Endurance methodology.
|
|
58
|
+
|
|
59
|
+
### 1. The 7-Zone Prerequisite
|
|
60
|
+
80/20 uses a 7-level zone system with two crossover zones: `1, 2, X, 3, Y, 4, 5`.
|
|
61
|
+
Intervals.icu supports 7 zones (`Z1` to `Z7`). `eticu` maps them sequentially.
|
|
62
|
+
**You MUST update your Intervals.icu HR zones (and Power zones, if using `--cycling-power`) to match the numbers from the 80/20 zone calculator.**
|
|
63
|
+
|
|
64
|
+
| 80/20 Zone | Intervals.icu Zone |
|
|
65
|
+
|------------|--------------------|
|
|
66
|
+
| Zone 1 | Z1 |
|
|
67
|
+
| Zone 2 | Z2 |
|
|
68
|
+
| Zone X | Z3 |
|
|
69
|
+
| **Zone 3** | **Z4** |
|
|
70
|
+
| Zone Y | Z5 |
|
|
71
|
+
| Zone 4 | Z6 |
|
|
72
|
+
| Zone 5 | Z7 |
|
|
73
|
+
|
|
74
|
+
*Note: Because of crossover Zone X, 80/20's "Zone 3" maps to Intervals "Z4".*
|
|
75
|
+
|
|
76
|
+
### 2. The `--cycling-power` Option & ERG Mode
|
|
77
|
+
By default, 80/20 workouts use Heart Rate targets. However, platforms like **Zwift** in ERG mode ignore HR targets; they require **Power (%FTP)**.
|
|
78
|
+
|
|
79
|
+
If you ride on Zwift/TrainerRoad, pass the `--cycling-power` flag. This converts all `Ride` workouts to use Power zones.
|
|
80
|
+
* **Prerequisite**: For this to work, you MUST have your FTP and 7-level Power zones correctly configured in your Intervals.icu settings. If you don't, power targets will be meaningless.
|
|
81
|
+
|
|
82
|
+
## Core Behaviors
|
|
83
|
+
|
|
84
|
+
- **Metric Units**: All outputs are metric. Run and Ride distances are rounded to the nearest `0.1 km`. Swims are strictly handled in `meters` (`mtr` in Intervals.icu to distinguish from minutes).
|
|
85
|
+
- **Pool Lengths**: Only `25m` and `50m` swim workouts are fetched and processed. `25y` files are skipped.
|
|
86
|
+
- **Folder Layout**: Workouts are uploaded into three flat folders (created automatically if they don't exist):
|
|
87
|
+
- `80/20 - Run`
|
|
88
|
+
- `80/20 - Bike`
|
|
89
|
+
- `80/20 - Swim`
|
|
90
|
+
- **Idempotency**: The upload is completely idempotent. It matches workouts by exact name within the folder and skips identical workouts, preventing duplication if the script is run multiple times.
|
|
91
|
+
|
|
92
|
+
## Credits & License
|
|
93
|
+
This project was heavily inspired by the GPL-3.0 licensed `njg4ne/8020-intervals-icu` project, expanding its capabilities into a fully-typed Python package with deeper FIT file introspection.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Test results [RESOLVED]
|
|
2
|
+
|
|
3
|
+
PS C:\Users\melih\Development\8020 Fetcher> uv run ruff check .
|
|
4
|
+
All checks passed!
|
|
5
|
+
PS C:\Users\melih\Development\8020 Fetcher> uv run ruff format .
|
|
6
|
+
18 files left unchanged
|
|
7
|
+
PS C:\Users\melih\Development\8020 Fetcher> uv run pytest -q
|
|
8
|
+
........................................ [100%]
|
|
9
|
+
40 passed in 1.65s
|
|
10
|
+
PS C:\Users\melih\Development\8020 Fetcher> uv run mypy src
|
|
11
|
+
src\eticu\fit_parse.py:19: error: Skipping analyzing "fitdecode": module is installed, but missing library stubs or py.typed marker [import-untyped]
|
|
12
|
+
src\eticu\fit_parse.py:19: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
|
|
13
|
+
src\eticu\intervals_client.py:49: error: Unused "type: ignore" comment [unused-ignore]
|
|
14
|
+
src\eticu\intervals_client.py:50: error: Returning Any from function declared to return "list[dict[str, Any]]" [no-any-return]
|
|
15
|
+
src\eticu\intervals_client.py:77: error: Unused "type: ignore" comment [unused-ignore]
|
|
16
|
+
src\eticu\intervals_client.py:77: error: Returning Any from function declared to return "list[dict[str, Any]]" [no-any-return]
|
|
17
|
+
src\eticu\intervals_client.py:77: note: Error code "no-any-return" not covered by "type: ignore[return-value]" comment
|
|
18
|
+
Found 5 errors in 2 files (checked 11 source files)
|
|
19
|
+
|
|
20
|
+
# Loop issue [RESOLVED]
|
|
21
|
+
|
|
22
|
+
- 8020/Z1 1s Z1
|
|
23
|
+
- 8020/Z2 1s Z2
|
|
24
|
+
2x
|
|
25
|
+
- 8020/Z3 1s Z4
|
|
26
|
+
- 8020/Z1 0s Z1
|
|
27
|
+
|
|
28
|
+
- 8020/Z2 1s Z2
|
|
29
|
+
- 8020/Z1 2s Z1
|
|
30
|
+
|
|
31
|
+
This is from intervals.icu
|
|
32
|
+
|
|
33
|
+
It is wrong, there should be a empty space before 2x so that loop is introduced correctly.
|
|
34
|
+
|
|
35
|
+
# Duration issue [RESOLVED]
|
|
36
|
+
|
|
37
|
+
for example in RF3:
|
|
38
|
+
|
|
39
|
+
- 8020/Z1 0s Z1 HR
|
|
40
|
+
- 8020/Z2 1s Z2 HR
|
|
41
|
+
- 8020/Z1 0s Z1 HR
|
|
42
|
+
|
|
43
|
+
(Fixed: FIT decode scaling for duration_value was incorrectly assumed to always be milliseconds. Duration parsing is now extracted as raw floating-point values since fitdecode automatically applies profile multipliers (producing seconds and meters directly).)
|
|
44
|
+
|
|
45
|
+
# Swim issue [RESOLVED]
|
|
46
|
+
|
|
47
|
+
INFO HTTP Request: GET https://intervals.icu/api/v1/athlete/i632678/folders "HTTP/1.1 200 OK"
|
|
48
|
+
INFO HTTP Request: POST https://intervals.icu/api/v1/athlete/i632678/workouts "HTTP/1.1 200 OK"
|
|
49
|
+
INFO Created workout 'SSI2 50m' (id=1064)
|
|
50
|
+
WARNING Step 4: no zone found in notes None — defaulting to Z1
|
|
51
|
+
WARNING Step has no zone; defaulting to 8020/Z1 (Z1)
|
|
52
|
+
WARNING Step has no zone; defaulting to 8020/Z1 (Z1)
|
|
53
|
+
WARNING Step has no zone; defaulting to 8020/Z1 (Z1)
|
|
54
|
+
|
|
55
|
+
(Fixed: convert.py no longer logs a warning for unzoned steps if their intensity is 'rest' or 'recovery', as these steps correctly don't require zones.)
|
|
56
|
+
|
|
57
|
+
# Too much workouts [RESOLVED]
|
|
58
|
+
|
|
59
|
+
INFO HTTP Request: POST https://intervals.icu/api/v1/athlete/i632678/workouts "HTTP/1.1 200 OK"
|
|
60
|
+
INFO Created workout 'STT2 50m' (id=1100)
|
|
61
|
+
created=1100 updated=0 skipped=0 error=0
|
|
62
|
+
|
|
63
|
+
while scrape says
|
|
64
|
+
|
|
65
|
+
Total: 550 (Run=190 Ride=218 Swim=142)
|
|
66
|
+
|
|
67
|
+
So everythin is posted twice
|
|
68
|
+
|
|
69
|
+
# Intervals ICU Syntax Guide [RESOLVED]
|
|
70
|
+
|
|
71
|
+
Use https://forum.intervals.icu/t/workout-builder-syntax-quick-guide/123701 for syntax. Reevaluate existing base with that guide.
|
|
72
|
+
|
|
73
|
+
(Evaluated and confirmed that the loops correctly use the empty line spacing syntax outlined in the guide, avoiding nested loop issues and correctly formatting `x` repeat blocks.)
|
|
74
|
+
|