Metadata-Version: 2.5
Name: intervals-ftp-update
Version: 0.1.0
Summary: Keep your intervals.icu FTP in sync with your eFTP, without letting decay drag it down
Project-URL: Homepage, https://github.com/bartTC/intervals-ftp-update
Project-URL: Issues, https://github.com/bartTC/intervals-ftp-update/issues
Author: Martin Mahner
License-Expression: MIT
License-File: LICENSE
Keywords: cycling,eftp,ftp,intervals.icu,training
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: python-dotenv>=1.0
Requires-Dist: rich>=13.7
Description-Content-Type: text/markdown

# intervals-ftp-update

Keeps the FTP on your [intervals.icu](https://intervals.icu) profile in sync with
the eFTP it estimates from your rides — without letting a quiet training block
quietly drag your FTP down with it.

```
uvx intervals-ftp-update --api-key=YOUR_KEY
```

That is the whole install. `uvx` fetches it, runs it, and throws it away again.
No athlete id needed — the key already identifies you.

## What it looks like

![intervals-ftp-update showing the eFTP history and its verdict](https://raw.githubusercontent.com/bartTC/intervals-ftp-update/main/docs/screenshot.png)

The **Change** column is the whole point: `+8` means a ride actually pushed
the eFTP up that day, `decay` means the number drifted down on its own
because an older peak aged out. Only the former counts as evidence.

## Why not just copy the eFTP across

The eFTP only moves up when a genuine best effort pushes it there. After that it
decays on its own, because the peak that produced it slowly ages out of the
90-day window. Nothing about you got slower — the number just ran out of
evidence.

A naive "set FTP to eFTP" job follows that decay down and quietly rewrites your
training load with it. So this tool treats the two directions differently:

| Direction | Condition |
| --------- | --------- |
| **Up** | A ride within the last 30 days actually raised the eFTP. |
| **Down** | The eFTP stayed below your FTP for 45 days straight, across at least 5 rides, and never came back near it. |

Everything else is reported and left alone.

Why this matters: intervals.icu computes training load (TSS) from your *recorded*
FTP, not the eFTP. Set it too low and every ride looks harder than it was, your
CTL climbs, and you think you are fitter than you are. Too high and the reverse.

## Usage

```
uvx intervals-ftp-update --api-key=KEY       # the key on its own is enough
uvx intervals-ftp-update                     # or take it from the environment / .env
uvx intervals-ftp-update --dry-run           # show the proposal, never write
uvx intervals-ftp-update --yes               # write without asking

# Only if you need to target someone other than the key's owner:
uvx intervals-ftp-update --api-key=KEY --athlete-id=123456
```

Nothing is written without a confirmation prompt unless you pass `--yes`.

### Credentials

The API key sits at the bottom of <https://intervals.icu/settings>. Provide it
by flag, by environment variable, or in a `.env` file next to where you run it:

```
INTERVALS_ICU_API_KEY=your_key_here
INTERVALS_ICU_ATHLETE_ID=123456
```

`INTERVALS_ICU_ATHLETE_ID` is optional — left out, the tool acts on whoever owns
the key. If you do pass it, digits only: the web UI writes it as `i123456` but
the API only accepts `123456`. A leading `i` is stripped for you.

### Options

| Flag | Default | Meaning |
| ---- | ------- | ------- |
| `--api-key` | `$INTERVALS_ICU_API_KEY` | Your intervals.icu API key |
| `--athlete-id` | the key's own athlete | Digits only; a leading `i` is stripped |
| `--sport` | `Ride` | Which sport settings to read the FTP from |
| `--threshold` | `5` | Minimum gap in watts before anything happens |
| `--confirm-days` | `30` | Window in which a rise must have been confirmed |
| `--decay-days` | `45` | Window that must stay low before lowering |
| `--min-rides` | `5` | Minimum rides in that window |
| `--lookback` | `365` | How far back to read history |
| `--dry-run` | | Show the proposal, never write |
| `--yes` | | Skip the confirmation prompt |

## What it does not touch

Changing your FTP does **not** rewrite past activities. Every ride on
intervals.icu stores the FTP it was computed with, so your fitness curve stays
intact. This tool never calls the `sport-settings/{id}/apply` endpoint, which is
the one that *would* retroactively recompute your history.

HR zones are left alone too — they hang off your LTHR, not your FTP.

## Development

```
just test       # run the suite with coverage
just coverage   # the same, as a browsable HTML report
just check      # dry run against your own account
just update     # run for real, with the confirmation prompt
just build      # build the distribution
```

Releases are cut by tagging:

```
git tag v0.1.0 && git push --tags
```

CI runs the tests, refuses a tag that disagrees with the version in
`pyproject.toml`, and publishes to PyPI through Trusted Publishing — no
API token is stored in the repository.

## License

MIT
