diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 8dda238..7f6b196 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -7,6 +7,9 @@ on: - '!master' workflow_dispatch: +permissions: + actions: read + jobs: run-cute-pets: runs-on: ubuntu-latest @@ -22,6 +25,30 @@ jobs: - name: Install dependencies run: pip install --break-system-packages -r requirements.txt + - name: Get Previous Run ID + continue-on-error: true + id: get_id + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Fetches the ID of the last completed run for the current workflow + PREVIOUS_RUN_ID=$(gh run list \ + --branch "${{ github.ref_name }}" \ + --workflow "${{ github.workflow }}" \ + --status success \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId') + echo "previous_run_id=$PREVIOUS_RUN_ID" >> "$GITHUB_OUTPUT" + + - name: Download previous database artifact + uses: actions/download-artifact@v8 + with: + name: database.json + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ steps.get_id.outputs.previous_run_id }} + continue-on-error: true + - name: Call RescueGroups API env: CUTEPETSBOSTON_RESCUEGROUPS_API_KEY: ${{ secrets.CUTEPETSBOSTON_RESCUEGROUPS_API_KEY }} @@ -34,3 +61,10 @@ jobs: run: | #In order to create posts on the test accounts remove the --debugposters debug flag python ./main.py --debugsources --debugposters + + - name: Upload database artifact + uses: actions/upload-artifact@v7 + with: + path: database.json + retention-days: 1 + archive: false diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 299987f..5d58fb6 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -6,6 +6,9 @@ on: # Every 4 hours - cron: "0 */4 * * *" +permissions: + actions: read + jobs: run-cute-pets: runs-on: ubuntu-latest @@ -21,6 +24,30 @@ jobs: - name: Install dependencies run: pip install --break-system-packages -r requirements.txt + - name: Get Previous Run ID + continue-on-error: true + id: get_id + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Fetches the ID of the last completed run for the current workflow + PREVIOUS_RUN_ID=$(gh run list \ + --branch "${{ github.ref_name }}" \ + --workflow "${{ github.workflow }}" \ + --status success \ + --limit 1 \ + --json databaseId \ + --jq '.[0].databaseId') + echo "previous_run_id=$PREVIOUS_RUN_ID" >> "$GITHUB_OUTPUT" + + - name: Download previous database artifact + uses: actions/download-artifact@v8 + with: + name: database.json + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ steps.get_id.outputs.previous_run_id }} + continue-on-error: true + - name: Call RescueGroups API env: CUTEPETSBOSTON_RESCUEGROUPS_API_KEY: ${{ secrets.CUTEPETSBOSTON_RESCUEGROUPS_API_KEY }} @@ -32,3 +59,10 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} APP_ENV: prod run: python ./main.py + + - name: Upload database artifact + uses: actions/upload-artifact@v7 + with: + path: database.json + retention-days: 14 + archive: false diff --git a/adoption_sources/manual.py b/adoption_sources/manual.py index ef958a4..ea9cb36 100644 --- a/adoption_sources/manual.py +++ b/adoption_sources/manual.py @@ -36,6 +36,7 @@ def fetch_pets(self) -> Iterable[AdoptablePet]: def _build_pet(self, animal: dict) -> AdoptablePet: attrs = animal.get("attributes", {}) + animal_id = animal.get("id", "") return AdoptablePet( name=attrs.get("name", "Unknown"), species=self.species, @@ -44,6 +45,7 @@ def _build_pet(self, animal: dict) -> AdoptablePet: description=(attrs.get("descriptionText") or "").strip(), adoption_url=self._adoption_url(attrs.get("slug")), image_url=attrs.get("pictureThumbnailUrl"), + pet_id=animal_id, ) @staticmethod diff --git a/main.py b/main.py index a0f0701..d281dee 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,11 @@ import os import random import argparse +import json +import sys import traceback +from pathlib import Path +from datetime import datetime, timezone, timedelta import requests @@ -87,10 +91,40 @@ def run(sources, posters): def pick_pet(pets): - eligible = [pet for pet in pets if pet.image_url and pet.adoption_url] - if not eligible: - return None - return random.choice(eligible) + Path("database.json").touch(exist_ok=True) + # Open file + with open("database.json", "r+") as f: + # Load json + try: + data = json.load(f) + except (json.JSONDecodeError, ValueError) as e: + print(f"{type(e).__name__}:{e}", file=sys.stderr) + traceback.print_exc() + data = {} + + if "posted_pets" in data: + posted_pet_ids = {posted_pet["pet_id"] for posted_pet in data["posted_pets"]} + else: + posted_pet_ids = {} + data["posted_pets"] = [] + # Check pet has an image, adoption url, and has not been posted + eligible = [pet for pet in pets if pet.image_url and pet.adoption_url and pet.pet_id not in posted_pet_ids] + if not eligible: + sys.exit("Error: No elligible pets found.") + return None + + selected_pet = random.choice(eligible) + # Add pet ID to list of posted pets + data["posted_pets"].append({"name": selected_pet.name, "pet_id": selected_pet.pet_id, "time": datetime.now(timezone.utc).isoformat()}) + # Remove old pets + cutoff = datetime.now(timezone.utc) - timedelta(weeks=12) + recent_pets = [item for item in data["posted_pets"] if datetime.fromisoformat(item['time']) > cutoff] + data["posted_pets"] = recent_pets + # Export json + f.seek(0) + json.dump(data, f, indent=4) + f.truncate() + return selected_pet # Slack incoming-webhook messages have a ~40k-char limit; cap the traceback