Metadata-Version: 2.4
Name: pyrwgps
Version: 0.2.0
Summary: A Python client for the RideWithGPS API. Not affiliated with RideWithGPS.
Project-URL: Homepage, https://github.com/ckdake/pyrwgps
Project-URL: Changelog, https://github.com/ckdake/pyrwgps/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/ckdake/pyrwgps/issues
Project-URL: PyPI, https://pypi.org/project/pyrwgps/
Author-email: Chris Kelly <ckdake@ckdake.com>
License-Expression: MIT
License-File: LICENSE.txt
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Requires-Dist: certifi>=2026.1.4
Requires-Dist: urllib3>=2.6.3
Provides-Extra: dev
Requires-Dist: black==26.1.0; extra == 'dev'
Requires-Dist: build==1.4.0; extra == 'dev'
Requires-Dist: certifi==2026.1.4; extra == 'dev'
Requires-Dist: flake8==7.3.0; extra == 'dev'
Requires-Dist: mypy==1.19.1; extra == 'dev'
Requires-Dist: pylint==4.0.5; extra == 'dev'
Requires-Dist: pytest-cov==7.0.0; extra == 'dev'
Requires-Dist: pytest==9.0.2; extra == 'dev'
Requires-Dist: pyyaml==6.0.3; extra == 'dev'
Requires-Dist: twine==6.2.0; extra == 'dev'
Requires-Dist: urllib3==2.6.3; extra == 'dev'
Requires-Dist: vcrpy==8.1.1; extra == 'dev'
Description-Content-Type: text/markdown

# pyrwgps

A simple Python client for the [RideWithGPS API](https://ridewithgps.com/api/v1/doc).

*This project is not affiliated with or endorsed by RideWithGPS.*

> **Note:** This client is not yet feature-complete. Read the code before you use it and report any bugs you find.
>
> **API versions:** RideWithGPS has a v1 API (`/api/v1/...`) and a legacy API (no version prefix). This client supports both, but v1 coverage is incomplete. See [API Versions](#api-versions) for a full breakdown of what uses v1 vs legacy.

> **Authentication:** Two methods are supported via the same `RideWithGPS` class: API key and OAuth 2.0. See [Authentication](#authentication) for details.

[![PyPI version](https://img.shields.io/pypi/v/pyrwgps.svg)](https://pypi.org/project/pyrwgps/)
[![PyPI downloads](https://img.shields.io/pypi/dm/pyrwgps.svg)](https://pypi.org/project/pyrwgps/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python versions](https://img.shields.io/pypi/pyversions/pyrwgps.svg)](https://pypi.org/project/pyrwgps/)

[![black](https://github.com/ckdake/pyrwgps/actions/workflows/black.yml/badge.svg)](https://github.com/ckdake/pyrwgps/actions/workflows/black.yml)
[![flake8](https://github.com/ckdake/pyrwgps/actions/workflows/flake8.yml/badge.svg)](https://github.com/ckdake/pyrwgps/actions/workflows/flake8.yml)
[![mypy](https://github.com/ckdake/pyrwgps/actions/workflows/mypy.yml/badge.svg)](https://github.com/ckdake/pyrwgps/actions/workflows/mypy.yml)
[![pylint](https://github.com/ckdake/pyrwgps/actions/workflows/pylint.yml/badge.svg)](https://github.com/ckdake/pyrwgps/actions/workflows/pylint.yml)
[![pytest](https://github.com/ckdake/pyrwgps/actions/workflows/pytest.yml/badge.svg)](https://github.com/ckdake/pyrwgps/actions/workflows/pytest.yml)

## Features

- Single `RideWithGPS` client supporting both **API key** and **OAuth 2.0** authentication.
- Makes any API request — `get`, `put`, `post`, `patch`, `delete` — to v1 or legacy endpoints.
- Built-in rate limiting, caching, and pagination.
- Use higher-level abstractions like `list` to iterate collections with automatic pagination.

## API Versions

RideWithGPS has a v1 API (`/api/v1/...`) and a legacy API (no version prefix). This client supports
both. The v1 API is preferred where available, but several things are **not yet in v1** and still
require the legacy API.

### Uses v1 API (`/api/v1/...`)

| Resource | Operations |
|---|---|
| Authentication | Create token (email + password + API key, no OAuth needed) |
| Trips | List, get single, delete, get polyline |
| Routes | List, get single, delete, get polyline |
| Collections | List, get single, get pinned |
| Events | List, create, get single, update, delete |
| Club Members | List, get single, update |
| Points of Interest | List, create, get single, update, delete, associate/disassociate with route |
| Sync | Get items for sync |
| Users | Get current user |

For `list` over a v1 endpoint, pass `result_key` matching the response root key (e.g. `result_key="trips"`). See [v1 API usage](#v1-api) for examples.

### Still uses legacy API (no version prefix)

These are **not available on v1** and require the legacy endpoints:

| Resource / Feature | Legacy endpoint | Reason |
|---|---|---|
| Gear | `/users/{user_id}/gear.json` | No v1 gear endpoint exists |
| Create trip | `/trips.json` (POST) | v1 trips is read/delete only |
| Update trip | `/trips/{id}.json` (PUT/PATCH) | v1 trips is read/delete only |
| Create route | `/routes.json` (POST) | v1 routes is read/delete only |
| Update route | `/routes/{id}.json` (PUT/PATCH) | v1 routes is read/delete only |

## v1 API

v1 endpoints live under `/api/v1/` and use `page`/`page_size` pagination, returning results
under a named root key (e.g. `{"trips": [...], "meta": {...}}`).

All of `list`, `get`, `put`, `post`, `patch`, and `delete` work with v1 paths.
For `list`, pass `result_key` matching the response root key:

```python
# List all trips (v1)
for trip in client.list("/api/v1/trips.json", result_key="trips"):
    print(trip.name, trip.id)

# List routes, up to 50 (v1)
for route in client.list("/api/v1/routes.json", result_key="routes", limit=50):
    print(route.name, route.id)

# Get a single resource (v1)
route = client.get(path="/api/v1/routes/123456.json")
print(route.route.name)

# Get the authenticated user's pinned collection (v1)
pinned = client.get(path="/api/v1/collections/pinned.json")

# Create an event (v1)
event = client.post(
    path="/api/v1/events.json",
    params={"name": "My Gran Fondo", "start_date": "2026-06-01"},
)
```

## Authentication

`RideWithGPS` supports two authentication methods. Pass the appropriate credentials at init time.

### API key (shared secret)

Simple option for scripts or server-side access to your own account.

```python
from pyrwgps import RideWithGPS

client = RideWithGPS(apikey="yourapikey")
user = client.authenticate(email="your@email.com", password="yourpassword")
```

### OAuth 2.0

For applications acting on behalf of users without handling their password. Requires a registered
API client — see [RideWithGPS authentication docs](https://ridewithgps.com/api/v1/doc/authentication).

**Full authorization code flow:**

```python
from pyrwgps import RideWithGPS

client = RideWithGPS(client_id="your_client_id", client_secret="your_client_secret")

# Step 1: redirect the user to this URL
auth_url = client.authorization_url(redirect_uri="https://yourapp.com/callback")
print(f"Redirect user to: {auth_url}")

# Step 2: after the user grants access, exchange the code from ?code=...
client.exchange_code(code="code_from_redirect", redirect_uri="https://yourapp.com/callback")

# Step 3: make authenticated API calls
user = client.get(path="/api/v1/users/current.json")
print(user.user.display_name)
```

**With an existing access token:**

```python
client = RideWithGPS(
    client_id="your_client_id",
    client_secret="your_client_secret",
    access_token="your_stored_token",
)
user = client.get(path="/api/v1/users/current.json")
```

Both auth methods expose the same `get`, `put`, `post`, `patch`, `delete`, and `list` methods.

---

## Installation

The package is published on [PyPI](https://pypi.org/project/pyrwgps/).

---

## Usage

First, install the package:

```sh
pip install pyrwgps
```

Then, in your Python code (API key example — see [Authentication](#authentication) for both methods):

```python
from pyrwgps import RideWithGPS

# Initialize client and authenticate, with optional in-memory GET cache enabled
client = RideWithGPS(apikey="yourapikey", cache=True)
user_info = client.authenticate(email="your@email.com", password="yourpassword")

print(user_info.id, user_info.display_name)

# Update the name of a trip (legacy API — trip update not yet in v1)
activity_id = "123456"
new_name = "Morning Ride"
response = client.put(
    path=f"/trips/{activity_id}.json",
    params={"name": new_name}
)
updated_name = response.trip.name if hasattr(response, "trip") else None
if updated_name == new_name:
    print(f"Activity name updated to: {updated_name}")
else:
    print("Failed to update activity name.")

# We changed something, so probably should clear the cache.
client.clear_cache()

# Simple GET: fetch a single trip via the v1 API
trip = client.get(path="/api/v1/trips/123456.json")
print(trip.trip.name, trip.trip.id)

# Automatically paginate: List up to 25 trips via the v1 API
for trip in client.list(
    path="/api/v1/trips.json",
    result_key="trips",
    limit=25,
):
    print(trip.name, trip.id)

# Automatically paginate: List all routes via the v1 API
for route in client.list(
    path="/api/v1/routes.json",
    result_key="routes",
):
    print(route.name, route.id)

# Get the authenticated user's pinned collection (v1)
pinned = client.get(path="/api/v1/collections/pinned.json")
print(pinned)

# Automatically paginate: List all gear for this user (legacy API — no v1 gear endpoint yet)
gear = {}
for g in client.list(
    path=f"/users/{user_info.id}/gear.json",
    params={},
):
    gear[g.id] = g.nickname
print(gear)
```

**Note:**  
- All API responses are automatically converted from JSON to Python objects with attribute access.
- You must provide your own RideWithGPS credentials and API key.
- Use v1 endpoints (`/api/v1/...`) for trips, routes, collections, events, club members, points of interest, and sync. See the [v1 API section](#v1-api) for what is and isn't available.
- The `list`, `get`, `put`, `post`, `patch` and `delete` methods are the recommended interface for making API requests; see the code and [RideWithGPS API docs](https://ridewithgps.com/api/v1/doc) for available endpoints and parameters.

---

## Development

### Set up environment

If you use this as VS Dev Container, you can skip using a venv.

```sh
python3 -m venv env
source env/bin/activate
pip install .[dev]
```

Or, for local development with editable install:

```sh
git clone https://github.com/ckdake/pyrwgps.git
cd pyrwgps
pip install -e '.[dev]'
```

### Run tests

```sh
python -m pytest --cov=pyrwgps --cov-report=term-missing -v
```

### Run an example
```sh
python3 scripts/example.py
```

### Linting and Formatting

Run these tools locally to check and format your code:

- **pylint** (static code analysis):

    ```sh
    pylint pyrwgps
    ```

- **flake8** (style and lint checks):

    ```sh
    flake8 pyrwgps
    ```

- **black** (auto-formatting):

    ```sh
    black pyrwgps
    ```

- **mypy** (type checking):

    ```sh
    mypy pyrwgps
    ```

### Updating Integration Cassettes

If you need to update the VCR cassettes for integration tests:

1. **Set required environment variables:**
   - `RIDEWITHGPS_EMAIL`
   - `RIDEWITHGPS_PASSWORD`
   - `RIDEWITHGPS_KEY`

   Example:
   ```sh
   export RIDEWITHGPS_EMAIL=your@email.com
   export RIDEWITHGPS_PASSWORD=yourpassword
   export RIDEWITHGPS_KEY=yourapikey
   ```

2. **Delete all existing cassettes (including backups) and re-record:**
   ```sh
   rm tests/cassettes/*.yaml tests/cassettes/*.yaml.original
   python -m pytest --cov=pyrwgps --cov-report=term-missing -v
   ```

3. **Scrub sensitive data from the cassette:**
   ```sh
   python scripts/scrub_cassettes.py
   ```
   - This will back up your cassettes to `*.yaml.original` (if not already present).
   - The sanitized cassettes will overwrite `*.yaml`.

4. **Re-run tests to verify:**
   ```sh
   python -m pytest --cov=pyrwgps --cov-report=term-missing -v
   ```

### Publishing a Release

To publish a new version of this package:

1. **Update the version number**  
   Edit `pyproject.toml` and increment the version. Add a new entry to [CHANGELOG.md](CHANGELOG.md).

2. **Commit and push**  
   ```sh
   git add pyproject.toml CHANGELOG.md
   git commit -m "Release vX.Y.Z"
   git push
   ```

3. **Install build tools**  
   ```sh
   pip install .[dev]
   ```

4. **Build the distribution**  
   ```sh
   python -m build
   ```
   This will create `dist/pyrwgps-<version>.tar.gz` and `.whl` files.

5. **Check the distribution (optional but recommended)**  
   ```sh
   twine check dist/*
   ```

6. **Upload to PyPI**  
   ```sh
   twine upload dist/*
   ```
   You will be prompted for your PyPI API key.

7. **Create a GitHub Release**  
   Go to [github.com/ckdake/pyrwgps/releases/new](https://github.com/ckdake/pyrwgps/releases/new),
   create a tag (e.g. `vX.Y.Z`) pointing at `main`, use the version as the title, and paste the
   relevant section from [CHANGELOG.md](CHANGELOG.md) as the description. This populates the
   Releases sidebar on the repo and gives users a permanent URL for the release notes.

8. **Open your package on PyPI (optional)**  
   ```sh
   $BROWSER https://pypi.org/project/pyrwgps/
   ```

**Note:**  
- Configure your `~/.pypirc` if you want to avoid entering credentials each time.
- For test uploads, use `twine upload --repository testpypi dist/*` and visit [TestPyPI](https://test.pypi.org/).

---

- [PyPI: pyrwgps](https://pypi.org/project/pyrwgps/)
- [Changelog](CHANGELOG.md)
- [RideWithGPS API documentation](https://ridewithgps.com/api/v1/doc)

---

## License

MIT License

---

*This project is not affiliated with or endorsed by RideWithGPS.*
