From d1157276402abbcebc6b0114635f1282b2cca65f Mon Sep 17 00:00:00 2001 From: Welbert Castro Date: Sun, 17 May 2026 20:50:41 -0300 Subject: [PATCH 1/2] ci: add auto-label job for conventional commit types Co-authored-by: Cursor --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ced39a..f5042c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,22 @@ concurrency: permissions: contents: read + pull-requests: write jobs: + auto-label: + name: Auto-label PR + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: bcoe/conventional-release-labels@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ignored_types: '["chore"]' + type_labels: '{"feat":"feature","fix":"fix","perf":"performance","refactor":"refactor","docs":"documentation","test":"test","build":"build","ci":"ci","breaking_change":"breaking change"}' + lint-and-build: name: Lint and build runs-on: ubuntu-latest From fc7c58d7a11fc514e3a363728b81f8e6b84b5239 Mon Sep 17 00:00:00 2001 From: Welbert Castro Date: Sun, 17 May 2026 20:50:49 -0300 Subject: [PATCH 2/2] docs: add getting-started, cli, configuration, and architecture sections - getting-started.md: installation, token setup, sync/async quick-start - cli.md: full reference for search, info, and pull with all flags - configuration.md: GP_ACCESS_TOKEN setup with tabbed browser instructions - ARCHITECTURE.md: layer overview, data-flow sequence, and exception hierarchy using Mermaid diagrams - docs/index.md: quick-links card grid pointing to every section - mkdocs.yml: wire new pages into nav; enable admonition, tabbed, superfences (Mermaid), emoji, toc, and richer mkdocstrings options Co-authored-by: Cursor --- docs/ARCHITECTURE.md | 144 ++++++++++++++++++++++++++++++++++++++++ docs/cli.md | 131 ++++++++++++++++++++++++++++++++++++ docs/configuration.md | 67 +++++++++++++++++++ docs/getting-started.md | 124 ++++++++++++++++++++++++++++++++++ docs/index.md | 48 ++++++++++++++ mkdocs.yml | 29 ++++++++ 6 files changed, 543 insertions(+) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/cli.md create mode 100644 docs/configuration.md create mode 100644 docs/getting-started.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..d55bbf6 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,144 @@ +# Architecture + +This page describes the internal design of **gopro-api** — how the layers fit together, how data flows from an HTTP request to a typed Python object, and where to look when you want to extend the library. + +## High-level overview + +```mermaid +flowchart TD + U(["User / Script / CLI"]) + + U --> GC["GoProClient\n(sync wrapper)"] + U --> AGC["AsyncGoProClient\n(async wrapper)"] + + GC -->|delegates to| GA["GoProAPI\n(requests-based)"] + AGC -->|delegates to| AGA["AsyncGoProAPI\n(aiohttp-based)"] + + GA -->|HTTP| API(["api.gopro.com"]) + AGA -->|HTTP| API +``` + +## Package layout + +| Path | Role | +|------|------| +| `gopro_api/api/gopro.py` | `GoProAPI` — synchronous HTTP client (`requests`) | +| `gopro_api/api/async_gopro.py` | `AsyncGoProAPI` — asynchronous HTTP client (`aiohttp`) | +| `gopro_api/api/models.py` | Pydantic request/response models | +| `gopro_api/api/__init__.py` | Re-exports `GoProAPI`, `AsyncGoProAPI` | +| `gopro_api/client.py` | `GoProClient` / `AsyncGoProClient` — high-level wrappers | +| `gopro_api/config.py` | pydantic-settings `Settings` (`GP_ACCESS_TOKEN`) | +| `gopro_api/exceptions.py` | Custom exception hierarchy | +| `gopro_api/utils.py` | Shared helpers (resolution scoring, etc.) | +| `gopro_api/cli/` | Typer application — `search`, `info`, `pull` commands | + +## Layers + +### 1. HTTP clients (`gopro_api.api`) + +`GoProAPI` and `AsyncGoProAPI` are thin wrappers around HTTP calls. They: + +- Inject the `gp_access_token` cookie on every request. +- Validate and deserialise responses into Pydantic models. +- Raise typed exceptions (`gopro_api.exceptions`) on non-2xx responses. + +Both are context-managers that handle session lifecycle: + +```python +with GoProAPI() as api: # opens a requests.Session + result = api.search(...) + +async with AsyncGoProAPI() as api: # opens an aiohttp.ClientSession + result = await api.search(...) +``` + +### 2. High-level clients (`gopro_api.client`) + +`GoProClient` and `AsyncGoProClient` add convenience on top of the raw API: + +- **Pagination** — `--all-pages` iteration over `search` results. +- **Asset selection** — picking the best video variation by resolution. +- **File download** — streaming CDN downloads to disk. + +### 3. Pydantic models (`gopro_api.api.models`) + +All request parameters and API responses are typed with [Pydantic v2](https://docs.pydantic.dev/) models: + +- **Requests** — `GoProMediaSearchParams`, `CapturedRange`. +- **Responses** — `GoProMediaSearchResponse`, `GoProMediaDownloadResponse`, and their `_embedded` / `_pages` children (with field aliases for the GoPro API's underscore-prefixed keys). + +List fields in request models are serialised to comma-separated strings automatically when calling `model_dump()`. + +### 4. Configuration (`gopro_api.config`) + +A single `Settings` class (pydantic-settings) reads `GP_ACCESS_TOKEN` from: + +1. Environment variables. +2. A `.env` file in the current working directory. + +Clients accept an explicit `access_token` parameter that overrides `Settings`. + +### 5. CLI (`gopro_api.cli`) + +The CLI is built with [Typer](https://typer.tiangolo.com/) and [Rich](https://github.com/Textualize/rich): + +- `gopro_api/cli/app.py` — Typer application and shared options. +- `gopro_api/cli/search.py` — `search` command + `SearchPrinter`. +- `gopro_api/cli/info.py` — `info` command + `InfoPrinter`. +- `gopro_api/cli/pull.py` — `pull` command + `PullPrinter`. +- `gopro_api/cli/_common.py` — shared helpers. + +Each command delegates to `GoProClient`/`AsyncGoProClient` and passes the result to a dedicated `*Printer` class for Rich-formatted output. + +## Data flow — `gopro-api search` + +```mermaid +sequenceDiagram + actor User + participant CLI + participant GoProClient + participant GoProAPI + participant GoproCom as api.gopro.com + + User->>CLI: gopro-api search --start … --end … + CLI->>GoProClient: search() / search_all() + GoProClient->>GoProAPI: search(GoProMediaSearchParams) + GoProAPI->>GoproCom: GET /media/search?captured_after=…&per_page=… + GoproCom-->>GoProAPI: JSON response + GoProAPI-->>GoProClient: GoProMediaSearchResponse (Pydantic) + GoProClient-->>CLI: response + CLI->>User: Rich table or raw JSON (stdout) +``` + +## Error handling + +```mermaid +classDiagram + Exception <|-- GoProAPIError + GoProAPIError <|-- GoProAuthError + GoProAPIError <|-- GoProNotFoundError + + class GoProAPIError { + Base class for all library errors + } + class GoProAuthError { + HTTP 401 — token expired or missing + } + class GoProNotFoundError { + HTTP 404 — media ID does not exist + } +``` + +| Exception | Raised when | +|-----------|-------------| +| `GoProAPIError` | Base class for all library errors. | +| `GoProAuthError` | The API returns **401 Unauthorized** (token expired or missing). | +| `GoProNotFoundError` | The API returns **404** (media ID does not exist). | + +See `gopro_api/exceptions.py` and [API Reference → Exceptions](api/exceptions.md) for the full hierarchy. + +## Extending the library + +**New API endpoint** — add a method to `GoProAPI` / `AsyncGoProAPI` (and the corresponding Pydantic models), then expose it in `GoProClient` / `AsyncGoProClient`. + +**New CLI command** — create a new file under `gopro_api/cli/`, define a Typer command, register it in `app.py`, and add a `*Printer` class for output formatting. diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..e44861f --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,131 @@ +# CLI + +`gopro-api` ships with a command-line interface built on [Typer](https://typer.tiangolo.com/) and [Rich](https://github.com/Textualize/rich). After installation it is available as `gopro-api` on your `PATH`, or you can invoke it directly as a module: + +```bash +python -m gopro_api.cli [options] +``` + +## Global options + +| Option | Default | Description | +|--------|---------|-------------| +| `--timeout` | `60` | Timeout in seconds for API calls and CDN downloads. | +| `--version` | — | Print the installed version and exit. | +| `--help` | — | Show help text. | + +## Commands + +### `search` + +Search your GoPro cloud library within a capture date range. + +```bash +gopro-api search --start 2026-03-01 --end 2026-03-03 +``` + +**Options** + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `--start` | `DATE` | **required** | Range start (inclusive), `YYYY-MM-DD`. | +| `--end` | `DATE` | **required** | Range end (inclusive), `YYYY-MM-DD`. | +| `--per-page` | `INT` | `100` | Items per API page. | +| `--all-pages` | flag | off | Fetch every page automatically. | +| `--json` | flag | off | Print the raw API JSON instead of the tabular view. | + +**Default output** — a `# _pages` summary line, then a tab-separated table with columns `id`, `type`, `captured_at`, `filename`, … (other API fields land in an `extra` JSON column). + +**`--json`** — pretty-prints the full API-shaped response; combined with `--all-pages` produces a JSON array of every page. + +```bash +# Tabular view, all pages +gopro-api search --start 2026-03-01 --end 2026-03-03 --all-pages + +# Raw JSON for a single page +gopro-api search --start 2026-03-01 --end 2026-03-03 --per-page 30 --json + +# Raw JSON, all pages +gopro-api search --start 2026-03-01 --end 2026-03-03 --json --all-pages +``` + +--- + +### `info` + +Show download metadata for a single media item. + +```bash +gopro-api info MEDIA_ID +``` + +**Arguments** + +| Argument | Description | +|----------|-------------| +| `MEDIA_ID` | The media identifier returned by `search`. | + +**Options** + +| Option | Description | +|--------|-------------| +| `--json` | Print the full API payload instead of the formatted summary. | + +**Default output** — filename on the first line, then one line per downloadable file showing size and CDN URL. + +```bash +gopro-api info MEDIA_ID --json +``` + +--- + +### `pull` + +Download asset(s) for a media item into a local directory. + +```bash +gopro-api pull MEDIA_ID ./downloads +``` + +**Arguments** + +| Argument | Description | +|----------|-------------| +| `MEDIA_ID` | The media identifier returned by `search`. | +| `DESTINATION` | Local directory; created if it does not exist. | + +**Options** + +| Option | Type | Description | +|--------|------|-------------| +| `--height` | `INT` | Target height in pixels (videos). The variation with the smallest squared pixel-delta is chosen; ties go to the larger resolution. | +| `--width` | `INT` | Target width in pixels (videos). Combined with `--height` when both are given. | + +**Behaviour by media type** + +- **Videos (`.mp4`)** — downloads one `variations` entry: the tallest by default, or the closest to the requested `--height` / `--width`. +- **Photos** — uses the `files` list; one request per file. + +```bash +# Default (tallest video variant) +gopro-api pull MEDIA_ID ./downloads + +# Closest to 1080 p +gopro-api pull MEDIA_ID ./downloads --height 1080 + +# Closest to 1920 × 1080 +gopro-api pull MEDIA_ID ./downloads --width 1920 --height 1080 +``` + +## Running without an installed entry point + +```bash +python -m gopro_api.cli search --start 2026-03-01 --end 2026-03-02 +python -m gopro_api.cli info MEDIA_ID +python -m gopro_api.cli pull MEDIA_ID ./out +python -m gopro_api.cli pull MEDIA_ID ./out --height 720 +``` + +## API reference + +For the auto-generated docstrings of the CLI internals see [API Reference → CLI](api/cli.md). diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..550e584 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,67 @@ +# Configuration + +`gopro-api` uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) to load settings from the environment and from a `.env` file in the current working directory. + +## Required settings + +| Variable | Description | +|----------|-------------| +| `GP_ACCESS_TOKEN` | Your GoPro cloud access token (the value of the `gp_access_token` browser cookie). | + +## `.env` file + +Create a `.env` file at the root of your project (or wherever you run the CLI from): + +```env +GP_ACCESS_TOKEN=your_token_here +``` + +!!! warning "Never commit `.env`" + Add `.env` to your `.gitignore`. The token grants full access to your GoPro cloud account. + +## Overriding the token in code + +Pass the token directly when constructing a client — this takes precedence over the environment: + +```python +from gopro_api.api import GoProAPI, AsyncGoProAPI + +with GoProAPI(access_token="your_token_here") as api: + ... + +async with AsyncGoProAPI(access_token="your_token_here") as api: + ... +``` + +## Retrieving `gp_access_token` from your browser + +Sign in to [gopro.com](https://gopro.com) or [quik.gopro.com](https://quik.gopro.com). The site sets a cookie named **`gp_access_token`**. + +=== "Chrome / Edge / Brave" + + 1. Open the site while logged in. + 2. Press **F12** to open DevTools → **Application** tab → **Cookies** → select the origin (e.g. `https://quik.gopro.com`). + 3. Copy the **Value** of `gp_access_token`. + +=== "Firefox" + + 1. Press **F12** → **Storage** tab → **Cookies** → select the origin. + 2. Copy the **Value** of `gp_access_token`. + +=== "Network panel (Chromium)" + + 1. Open **DevTools** → **Network** tab. + 2. Trigger any request to `api.gopro.com` (e.g. browse your media library). + 3. Click on the request → **Headers** → scroll to **Request Headers** → **Cookie**. + 4. Copy the value after `gp_access_token=` up to the next `;` (or end of the string). + +!!! tip "HttpOnly cookies" + If the cookie is **HttpOnly**, the Application panel will not show its value. Use the **Network panel** method instead. + +## Token expiry + +Tokens expire. If you receive a **401 Unauthorized** error, return to your browser and copy a fresh token value. + +## API reference + +See [`gopro_api.config`](api/utils.md) for the `Settings` class that loads these values. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..dd3dd69 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,124 @@ +# Getting Started + +This page walks you through installing **gopro-api**, setting up your access token, and making your first search request. + +## Prerequisites + +- Python **3.10** or newer +- A GoPro / Quik account with media in the cloud +- Your `gp_access_token` browser cookie (see [Configuration](configuration.md)) + +## Installation + +### From PyPI + +```bash +pip install gopro-api +``` + +### From source + +```bash +git clone https://github.com/himewel/gopro-api.git +cd gopro-api +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +pip install -e . +``` + +### From a local wheel + +```bash +pip install ./dist/gopro_api-*-py3-none-any.whl +``` + +## Set up your token + +Create a `.env` file in your working directory: + +```env +GP_ACCESS_TOKEN=your_token_here +``` + +The library loads it automatically via **pydantic-settings**. See [Configuration](configuration.md) for how to retrieve the token from your browser. + +## Quick start — CLI + +After installing, `gopro-api` is available on your `PATH`: + +```bash +# List media captured on a specific day +gopro-api search --start 2026-03-01 --end 2026-03-02 + +# Show download metadata for a single media item +gopro-api info MEDIA_ID + +# Download a video at the closest resolution to 1080 p +gopro-api pull MEDIA_ID ./downloads --height 1080 +``` + +See the full [CLI reference](cli.md) for all flags and options. + +## Quick start — Python library + +### Synchronous + +```python +from datetime import datetime + +from gopro_api.api import GoProAPI +from gopro_api.api.models import CapturedRange, GoProMediaSearchParams + +params = GoProMediaSearchParams( + captured_range=CapturedRange( + start=datetime.fromisoformat("2026-03-01"), + end=datetime.fromisoformat("2026-03-02"), + ), + per_page=50, + page=1, +) + +with GoProAPI() as api: + search = api.search(params) + for item in search.embedded.media: + meta = api.download(item.id) + print(meta.filename, len(meta.embedded.files), "files") +``` + +### Asynchronous + +```python +import asyncio +from datetime import datetime + +from gopro_api.api import AsyncGoProAPI +from gopro_api.api.models import CapturedRange, GoProMediaSearchParams + +params = GoProMediaSearchParams( + captured_range=CapturedRange( + start=datetime.fromisoformat("2026-03-01"), + end=datetime.fromisoformat("2026-03-02"), + ), + per_page=50, + page=1, +) + +async def main() -> None: + async with AsyncGoProAPI() as api: + search = await api.search(params) + for item in search.embedded.media: + meta = await api.download(item.id) + print(meta.filename, len(meta.embedded.files), "files") + +asyncio.run(main()) +``` + +## Next steps + +| Topic | Where to go | +|-------|------------| +| All CLI flags | [CLI](cli.md) | +| Token setup | [Configuration](configuration.md) | +| Project internals | [Architecture](ARCHITECTURE.md) | +| Full API reference | [API Reference](api/api.md) | diff --git a/docs/index.md b/docs/index.md index 612c7a5..5186f28 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,49 @@ --8<-- "README.md" + +--- + +## Quick links + +
+ +- :material-rocket-launch:{ .lg .middle } **Getting Started** + + --- + + Install the library, set up your token, and make your first request in minutes. + + [:octicons-arrow-right-24: Getting Started](getting-started.md) + +- :material-console:{ .lg .middle } **CLI Reference** + + --- + + Full reference for `gopro-api search`, `info`, and `pull` with all flags and examples. + + [:octicons-arrow-right-24: CLI](cli.md) + +- :material-cog:{ .lg .middle } **Configuration** + + --- + + How to set `GP_ACCESS_TOKEN` and retrieve the cookie from your browser. + + [:octicons-arrow-right-24: Configuration](configuration.md) + +- :material-floor-plan:{ .lg .middle } **Architecture** + + --- + + Layer diagrams, data-flow sequences, and a guide for extending the library. + + [:octicons-arrow-right-24: Architecture](ARCHITECTURE.md) + +- :material-code-tags:{ .lg .middle } **API Reference** + + --- + + Auto-generated docstrings for every public class, method, model, and exception. + + [:octicons-arrow-right-24: API Reference](api/api.md) + +
diff --git a/mkdocs.yml b/mkdocs.yml index a1aec59..56afcaf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,7 +41,28 @@ extra_css: - stylesheets/colorful.css markdown_extensions: + - admonition + - attr_list + - md_in_html + - pymdownx.details + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - toc: + permalink: true plugins: - search @@ -53,9 +74,17 @@ plugins: docstring_style: google show_root_heading: true members_order: source + show_symbol_type_heading: true + show_symbol_type_toc: true + merge_init_into_class: true + show_signature_annotations: true nav: - Home: index.md + - Getting Started: getting-started.md + - CLI: cli.md + - Configuration: configuration.md + - Architecture: ARCHITECTURE.md - API Reference: - API: api/api.md - Client: api/client.md