From c06ccdd3107248f8189e34022260b3d7c60bfe82 Mon Sep 17 00:00:00 2001 From: Welbert Castro Date: Sat, 23 May 2026 18:40:14 -0300 Subject: [PATCH 1/2] feat(config)!: load settings lazily via get_settings() Replace import-time GP_ACCESS_TOKEN snapshot with memoized get_settings() and ignore unknown env keys so shared .env files with Q2GOOGLE_* vars work. Co-authored-by: Cursor --- README.md | 4 ++-- gopro_api/api/async_gopro.py | 8 ++++---- gopro_api/api/gopro.py | 8 ++++---- gopro_api/cli/_common.py | 4 ++-- gopro_api/client.py | 4 ++-- gopro_api/config.py | 23 ++++++++++++++++++----- pyproject.toml | 2 +- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4bdc165..868066a 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ python -m gopro_api.cli pull MEDIA_ID ./out --height 720 ## Configuration -`gopro_api.config` reads settings from the environment and from a `.env` file in the current working directory via **pydantic-settings**. The only required setting is **`GP_ACCESS_TOKEN`**. +`gopro_api.config` reads settings from the environment and from a `.env` file in the current working directory via **pydantic-settings**. Use `get_settings()` to access the memoized singleton; the only required setting is **`GP_ACCESS_TOKEN`**. Example `.env`: @@ -201,7 +201,7 @@ List fields in search params are serialized to comma-separated strings when you | `gopro_api/api/async_gopro.py` | `AsyncGoProAPI` — async `search`, `download` | | `gopro_api/api/models.py` | Pydantic request/response models | | `gopro_api/api/__init__.py` | Re-exports `GoProAPI`, `AsyncGoProAPI` | -| `gopro_api/config.py` | pydantic-settings `Settings`, `GP_ACCESS_TOKEN` | +| `gopro_api/config.py` | pydantic-settings `Settings`, lazy `get_settings()` | | `gopro_api/cli/` | `gopro-api` CLI | | `setup.py` | Package metadata, dependencies, console entry point | diff --git a/gopro_api/api/async_gopro.py b/gopro_api/api/async_gopro.py index 55a28c7..a1bf1cf 100644 --- a/gopro_api/api/async_gopro.py +++ b/gopro_api/api/async_gopro.py @@ -2,7 +2,7 @@ import aiohttp -from gopro_api.config import GP_ACCESS_TOKEN +from gopro_api.config import get_settings from gopro_api.api.models import ( GoProMediaSearchParams, GoProMediaDownloadResponse, @@ -15,7 +15,7 @@ class AsyncGoProAPI: Use as an async context manager so an ``aiohttp.ClientSession`` is opened and closed around ``search`` and ``download``. Pass ``access_token`` to - override ``gopro_api.config.GP_ACCESS_TOKEN``. + override :func:`~gopro_api.config.get_settings`. """ def __init__(self, access_token: str | None = None, timeout: float = 10.0) -> None: @@ -23,10 +23,10 @@ def __init__(self, access_token: str | None = None, timeout: float = 10.0) -> No Args: access_token: ``gp_access_token`` cookie value; defaults to - ``gopro_api.config.GP_ACCESS_TOKEN``. + :attr:`~gopro_api.config.Settings.gp_access_token` from settings. timeout: Total client timeout in seconds for ``aiohttp``. """ - self.access_token = access_token or GP_ACCESS_TOKEN + self.access_token = access_token or get_settings().gp_access_token self._timeout = aiohttp.ClientTimeout(total=timeout) self._session: aiohttp.ClientSession | None = None diff --git a/gopro_api/api/gopro.py b/gopro_api/api/gopro.py index 58aa92b..5f2eb1e 100644 --- a/gopro_api/api/gopro.py +++ b/gopro_api/api/gopro.py @@ -2,7 +2,7 @@ import requests -from gopro_api.config import GP_ACCESS_TOKEN +from gopro_api.config import get_settings from gopro_api.api.models import ( GoProMediaDownloadResponse, GoProMediaSearchParams, @@ -15,7 +15,7 @@ class GoProAPI: Use as a context manager so a ``requests.Session`` is created and closed around ``search`` and ``download``. Pass ``access_token`` to override - ``gopro_api.config.GP_ACCESS_TOKEN``. + :func:`~gopro_api.config.get_settings`. """ def __init__(self, access_token: str | None = None, timeout: float = 10.0) -> None: @@ -23,10 +23,10 @@ def __init__(self, access_token: str | None = None, timeout: float = 10.0) -> No Args: access_token: ``gp_access_token`` cookie value; defaults to - ``gopro_api.config.GP_ACCESS_TOKEN``. + :attr:`~gopro_api.config.Settings.gp_access_token` from settings. timeout: Per-request timeout in seconds passed to ``requests``. """ - self.access_token = access_token or GP_ACCESS_TOKEN + self.access_token = access_token or get_settings().gp_access_token self._timeout = timeout self._session: requests.Session | None = None diff --git a/gopro_api/cli/_common.py b/gopro_api/cli/_common.py index 52d5ada..b9e22df 100644 --- a/gopro_api/cli/_common.py +++ b/gopro_api/cli/_common.py @@ -8,7 +8,7 @@ import typer from rich.table import Table -from gopro_api.config import GP_ACCESS_TOKEN +from gopro_api.config import get_settings _FIELD_LABELS: dict[str, str] = { "type": "media", @@ -36,7 +36,7 @@ def _require_token() -> None: Raises: typer.Exit: With code ``2`` if the token is missing. """ - if not GP_ACCESS_TOKEN: + if not get_settings().gp_access_token: typer.secho( "error: GP_ACCESS_TOKEN is not set. " "Add it to your environment or a .env file.", diff --git a/gopro_api/client.py b/gopro_api/client.py index 89ff86d..67bda61 100644 --- a/gopro_api/client.py +++ b/gopro_api/client.py @@ -52,7 +52,7 @@ def __init__( Args: access_token: ``gp_access_token`` cookie value; defaults to - ``gopro_api.config.GP_ACCESS_TOKEN``. + :func:`~gopro_api.config.get_settings`. timeout: Per-request HTTP timeout in seconds (API and CDN fetches). page_size: Default page size for ``iter_nonempty_search_pages``. max_items: Maximum rows returned by ``list_media_items``. @@ -256,7 +256,7 @@ def __init__( Args: access_token: ``gp_access_token`` cookie value; defaults to - ``gopro_api.config.GP_ACCESS_TOKEN``. + :func:`~gopro_api.config.get_settings`. timeout: Total ``aiohttp`` client timeout in seconds. page_size: Default page size for ``iter_nonempty_search_pages``. max_items: Maximum rows returned by ``list_media_items``. diff --git a/gopro_api/config.py b/gopro_api/config.py index 1280699..73f97c8 100644 --- a/gopro_api/config.py +++ b/gopro_api/config.py @@ -2,14 +2,16 @@ from __future__ import annotations +from functools import lru_cache + from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Application settings from the process environment and optional ``.env`` file. - Values are read at instantiation; use ``gopro_api.config.settings`` or the - ``GP_ACCESS_TOKEN`` alias for the token used by API clients and the CLI. + Values are read at instantiation; use :func:`get_settings` for the token used + by API clients and the CLI. Attributes: gp_access_token: GoPro cloud cookie value. Environment variable: @@ -19,12 +21,23 @@ class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", + extra="ignore", ) gp_access_token: str | None = None -settings = Settings() +@lru_cache +def get_settings() -> Settings: + """Return the process-wide settings singleton. + + The result is memoized. Call ``get_settings.cache_clear()`` before constructing + a new ``Settings`` instance when tests mutate the environment. + + Returns: + Parsed :class:`Settings` for the current process. + """ + return Settings() + -# Backward-compatible module-level alias used throughout the package. -GP_ACCESS_TOKEN = settings.gp_access_token +__all__ = ["Settings", "get_settings"] diff --git a/pyproject.toml b/pyproject.toml index 75caf8d..4901814 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "gopro-api" -version = "0.0.8" +version = "0.0.9" description = "Unofficial Python client for the GoPro cloud API (api.gopro.com): sync and async clients, Pydantic models, and a CLI." readme = "README.md" license = { file = "LICENSE" } From aba7504b50c97c19d4dd6e9a685f10596dfd680f Mon Sep 17 00:00:00 2001 From: Welbert Castro Date: Sat, 23 May 2026 18:53:27 -0300 Subject: [PATCH 2/2] fix(ci): use breaking key for conventional-release-labels The action expects type_labels.breaking, not breaking_change. 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..f8ab09d 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":"breaking change"}' + lint-and-build: name: Lint and build runs-on: ubuntu-latest