diff --git a/.github/workflows/cosmic-daily.yml b/.github/workflows/cosmic-daily.yml index 9a15feb..5629c52 100644 --- a/.github/workflows/cosmic-daily.yml +++ b/.github/workflows/cosmic-daily.yml @@ -12,6 +12,11 @@ on: required: true default: false type: boolean + schedule: + # 12:00 UTC (08:00 EDT / 07:00 EST) — APOD for the current US day is + # already published by then. Always opens a PR for human review; it + # never auto-merges. + - cron: "0 12 * * *" permissions: contents: write @@ -46,6 +51,18 @@ jobs: fi echo "value=$target" >> "$GITHUB_OUTPUT" + - name: Resolve publish flag + id: mode + shell: bash + run: | + # Manual runs use the publish input; scheduled runs always publish + # (i.e. generate + open a PR for review, never auto-merge). + if [ "${{ github.event_name }}" = "schedule" ]; then + echo "publish=true" >> "$GITHUB_OUTPUT" + else + echo "publish=${{ inputs.publish }}" >> "$GITHUB_OUTPUT" + fi + - name: Preview or generate APOD content id: apod if: always() @@ -55,7 +72,7 @@ jobs: shell: bash run: | set -euo pipefail - if [ "${{ inputs.publish }}" = "true" ]; then + if [ "${{ steps.mode.outputs.publish }}" = "true" ]; then python -m cosmic_daily generate --date "${{ steps.target.outputs.value }}" | tee /tmp/cosmic-daily.log else python -m cosmic_daily preview --date "${{ steps.target.outputs.value }}" | tee /tmp/cosmic-daily.log @@ -67,7 +84,7 @@ jobs: fi - name: Validate generated post - if: ${{ inputs.publish == true && steps.apod.outputs.post_path != '' }} + if: ${{ steps.mode.outputs.publish == 'true' && steps.apod.outputs.post_path != '' }} working-directory: tools/cosmic-daily shell: bash run: | @@ -75,7 +92,7 @@ jobs: python -m cosmic_daily check --post-path "${{ steps.apod.outputs.post_path }}" - name: Create pull request - if: ${{ inputs.publish == true && steps.apod.outputs.post_path != '' }} + if: ${{ steps.mode.outputs.publish == 'true' && steps.apod.outputs.post_path != '' }} uses: peter-evans/create-pull-request@v6 with: branch: "cosmic-daily/${{ steps.target.outputs.value }}" diff --git a/.gitignore b/.gitignore index ab2e1c3..44d8911 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ !.env.example __pycache__/ *.py[cod] +*.egg-info/ .pytest_cache/ .venv/ venv/ diff --git a/tools/cosmic-daily/cosmic_daily/nasa_client.py b/tools/cosmic-daily/cosmic_daily/nasa_client.py index bd7aae3..dc02f58 100644 --- a/tools/cosmic-daily/cosmic_daily/nasa_client.py +++ b/tools/cosmic-daily/cosmic_daily/nasa_client.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import time from dataclasses import dataclass from datetime import date from typing import Any, Dict, Optional @@ -10,6 +11,11 @@ APOD_ENDPOINT = "https://api.nasa.gov/planetary/apod" +# DEMO_KEY is a shared, rate-limited key; requests can be slow under load, so +# use a generous read timeout and retry a couple of times before giving up. +REQUEST_TIMEOUT = (10, 45) +MAX_ATTEMPTS = 3 +RETRY_BACKOFF_SECONDS = 5 @dataclass @@ -51,15 +57,25 @@ def fetch_apod(day: str | date | None = None, api_key: str | None = None) -> APO target_day = day.isoformat() if isinstance(day, date) else (str(day) if day else date.today().isoformat()) configured_key = api_key or os.getenv("NASA_API_KEY") or "DEMO_KEY" - try: - response = requests.get( - APOD_ENDPOINT, - params={"api_key": configured_key, "date": target_day}, - timeout=20, - allow_redirects=False, - ) - except requests.RequestException as exc: - raise RuntimeError(f"Failed to fetch APOD for {target_day}: {exc}") from exc + response = None + last_error: requests.RequestException | None = None + for attempt in range(1, MAX_ATTEMPTS + 1): + try: + response = requests.get( + APOD_ENDPOINT, + params={"api_key": configured_key, "date": target_day}, + timeout=REQUEST_TIMEOUT, + allow_redirects=False, + ) + break + except requests.RequestException as exc: + last_error = exc + if attempt < MAX_ATTEMPTS: + time.sleep(RETRY_BACKOFF_SECONDS * attempt) + if response is None: + raise RuntimeError( + f"Failed to fetch APOD for {target_day} after {MAX_ATTEMPTS} attempts: {last_error}" + ) from last_error if response.status_code != 200: raise RuntimeError(f"NASA API returned HTTP {response.status_code} for {target_day}")