From 39527ee1b6ab6b30fe8967edf9274aa9ba8f9435 Mon Sep 17 00:00:00 2001 From: Nicconike <38905025+Nicconike@users.noreply.github.com> Date: Wed, 29 Jan 2025 02:52:12 +0530 Subject: [PATCH] build: update API URLs to use HTTPS and modify configuration files ci: ignore updates for 1.49.1 playwright docker image --- .github/dependabot.yml | 4 + .github/workflows/release.yml | 6 +- .gitignore | 3 + README.md | 4 +- api/card.py | 342 +++++++++++++++--------------- api/steam_stats.py | 4 +- assets/recently_played_games.html | 97 +++++---- assets/recently_played_games.png | Bin 65177 -> 72137 bytes assets/steam_summary.html | 140 ++++++------ assets/steam_summary.png | Bin 40814 -> 28871 bytes assets/steam_workshop_stats.html | 154 +++++++------- assets/steam_workshop_stats.png | Bin 21626 -> 13155 bytes pyproject.toml | 71 +++---- requirements.txt | Bin 470 -> 470 bytes tests/test_card.py | 49 +++-- tests/test_steam_stats.py | 14 +- tests/test_steam_workshop.py | 10 +- 17 files changed, 440 insertions(+), 458 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0fa4f07..c7b1ecd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -56,6 +56,10 @@ updates: - dependabot - docker rebase-strategy: auto + ignore: + - dependency-name: "playwright/python" + versions: + - "1.49.1" open-pull-requests-limit: 10 groups: docker: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 742676a..78e91c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: issues: write pull-requests: write outputs: - new_release_version: ${{ steps.semantic.outputs.version }} + new_release_version: ${{ steps.semantic.outputs.new_release_version }} steps: - name: GitHub App Token uses: actions/create-github-app-token@v1 @@ -73,12 +73,14 @@ jobs: uses: python-semantic-release/python-semantic-release@v9.17.0 with: github_token: ${{ steps.app-token.outputs.token }} + env: + PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring docker: name: Docker runs-on: ubuntu-latest needs: release - if: needs.release.outputs.new_release_version != '' || github.event_name == 'release' + if: needs.release.outputs.new_release_version != '' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} diff --git a/.gitignore b/.gitignore index 3451e37..e9b33fc 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # Vercel vercel.txt .vercel + +# pytest configuration +pytest.ini diff --git a/README.md b/README.md index 9aa2a19..949b8e6 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ ![Docker Pulls](https://img.shields.io/docker/pulls/nicconike/steam-stats?logo=docker&label=Docker%20Pulls&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fnicconike%2Fsteam-stats) ![GitHub Release](https://img.shields.io/github/v/release/nicconike/steam-stats) ![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fgithub.com%2FNicconike%2FSteam-Stats%2Fblob%2Fmaster%2Fpyproject.toml%3Fraw%3Dtrue) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9965/badge)](https://www.bestpractices.dev/projects/9965) ![GitHub License](https://img.shields.io/github/license/nicconike/Steam-Stats) -[![wakatime](https://wakatime.com/badge/user/018e538b-3f55-4e8e-95fa-6c3225418eed/project/018e62a4-056d-49fd-babd-b079ee94859f.svg)](https://wakatime.com/badge/user/018e538b-3f55-4e8e-95fa-6c3225418eed/project/018e62a4-056d-49fd-babd-b079ee94859f) [![Visitor Badge](https://badges.pufler.dev/visits/nicconike/steam-stats)](https://badges.pufler.dev) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9965/badge)](https://www.bestpractices.dev/projects/9965) +[![wakatime](https://wakatime.com/badge/user/018e538b-3f55-4e8e-95fa-6c3225418eed/project/018e62a4-056d-49fd-babd-b079ee94859f.svg)](https://wakatime.com/badge/user/018e538b-3f55-4e8e-95fa-6c3225418eed/project/018e62a4-056d-49fd-babd-b079ee94859f) > ### From one Passionate Gamer and Developer to Another 🍻 *** diff --git a/api/card.py b/api/card.py index 6465dd8..1d9fd34 100644 --- a/api/card.py +++ b/api/card.py @@ -5,6 +5,7 @@ import math import os import asyncio +from typing import Optional, TypedDict from playwright.async_api import async_playwright, Error as PlaywrightError # Configure logging @@ -13,13 +14,25 @@ ) logger = logging.getLogger(__name__) + MARGIN = 10 +CARD_SELECTOR = ".card" + + +class FloatRect(TypedDict): + """Define FloatRect type for compatibility with Playwright""" + + x: float + y: float + width: float + height: float -# Get Github Repo's Details where the action is being ran -repo_owner, repo_name = os.environ["GITHUB_REPOSITORY"].split("/") -branch_name = os.environ["GITHUB_REF_NAME"] -# Personastate mapping for Steam Profile Status +# Get GitHub Repo's Details where the action is being run +repo_owner, repo_name = os.getenv("GITHUB_REPOSITORY", "owner/repo").split("/") +branch_name = os.getenv("GITHUB_REF_NAME", "main") + +# Persona state mapping for Steam Profile Status personastate_map = { 0: "Offline", 1: "Online", @@ -34,113 +47,144 @@ def handle_exception(e): """Handle exceptions and log appropriate error messages""" if isinstance(e, FileNotFoundError): - logger.error("File Not Found Error: %s", str(e)) + logger.error("File Not Found Error: %s", e) elif isinstance(e, PlaywrightError): - logger.error("Playwright Error: %s", str(e)) + logger.error("Playwright Error: %s", e) elif isinstance(e, KeyError): - logger.error("Key Error: %s", str(e)) + logger.error("Key Error: %s", e) elif isinstance(e, asyncio.TimeoutError): - logger.error("Timeout Error: %s", str(e)) + logger.error("Timeout Error: %s", e) else: - logger.error("Unexpected Error: %s", str(e)) + logger.error("Unexpected Error: %s", e) -async def get_element_bounding_box(html_file, selector): +async def get_element_bounding_box( + html_file: str, selector: str, margin: int = MARGIN +) -> Optional[FloatRect]: """Get the bounding box of the specified element using Playwright""" browser = None try: # Check if the HTML file exists if not os.path.exists(html_file): - raise FileNotFoundError("HTML file not found:" + str(html_file)) + raise FileNotFoundError("HTML file not found: " + html_file) + async with async_playwright() as p: browser = await p.firefox.launch(headless=True) page = await browser.new_page() - await page.goto("file://" + os.path.abspath(html_file)) - bounding_box = await page.evaluate( - "() => {" - ' var element = document.querySelector("' + selector + '");' - " if (!element) {" - ' throw new Error("Element not found: ' + selector + '");' - " }" - " var rect = element.getBoundingClientRect();" - " return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};" - "}" - ) - await page.close() - # Add margin to the bounding box - bounding_box["x"] = max(bounding_box["x"] - MARGIN, 0) - bounding_box["y"] = max(bounding_box["y"] - MARGIN, 0) - bounding_box["width"] += 2 * MARGIN - bounding_box["height"] += 2 * MARGIN - return bounding_box - except (FileNotFoundError, PlaywrightError, KeyError, asyncio.TimeoutError) as e: + try: + await page.goto("file://" + os.path.abspath(html_file)) + except TimeoutError as e: + raise asyncio.TimeoutError("Timeout error while loading page") from e + + # Find element and get its bounding box + element = await page.query_selector(selector) + if not element: + raise ValueError("Element not found for selector: " + selector) + + try: + bounding_box = await element.bounding_box() + except KeyError as e: + raise KeyError("Key error while retrieving bounding box") from e + + if not bounding_box: + raise ValueError( + "Could not retrieve bounding box for selector: " + selector + ) + + # Add margin to the bounding box + bounding_box_with_margin: FloatRect = { + "x": max(bounding_box["x"] - margin, 0), + "y": max(bounding_box["y"] - margin, 0), + "width": bounding_box["width"] + 2 * margin, + "height": bounding_box["height"] + 2 * margin, + } + + await browser.close() + return bounding_box_with_margin + + except ( + FileNotFoundError, + PlaywrightError, + ValueError, + KeyError, + asyncio.TimeoutError, + ) as e: handle_exception(e) + return None finally: if browser: await browser.close() -async def html_to_png(html_file, output_file, selector): - """Convert HTML file to PNG using Playwright with clipping""" +async def html_to_png( + html_file: str, output_file: str, selector: str, margin: int = MARGIN +) -> bool: + """Convert an HTML file to a PNG using Playwright with clipping""" + bounding_box = await get_element_bounding_box(html_file, selector, margin) + if not bounding_box: + logger.error("Bounding box could not be determined") + return False + + clip: FloatRect = { + "x": float(bounding_box["x"]), + "y": float(bounding_box["y"]), + "width": float(bounding_box["width"]), + "height": float(bounding_box["height"]), + } + browser = None try: - bounding_box = await get_element_bounding_box(html_file, selector) async with async_playwright() as p: browser = await p.firefox.launch(headless=True) page = await browser.new_page() await page.goto("file://" + os.path.abspath(html_file)) - await page.screenshot(path=output_file, clip=bounding_box) - await page.close() - except (FileNotFoundError, PlaywrightError, KeyError, asyncio.TimeoutError) as e: + + # Take screenshot with clipping + await page.screenshot(path=output_file, clip=clip) + return True + + except (PlaywrightError, asyncio.TimeoutError) as e: handle_exception(e) + return False + finally: if browser: await browser.close() def convert_html_to_png(html_file, output_file, selector): - """Convert HTML file to PNG using Playwright with clipping""" + """Synchronous wrapper to convert HTML to PNG""" try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(html_to_png(html_file, output_file, selector)) + return asyncio.run(html_to_png(html_file, output_file, selector)) except (FileNotFoundError, PlaywrightError, KeyError, asyncio.TimeoutError) as e: handle_exception(e) + return False def format_unix_time(unix_time): """Convert Unix time to human-readable format with ordinal day""" dt = datetime.datetime.fromtimestamp(unix_time) day = dt.day - - if 11 <= day <= 13: - suffix = "th" - else: - suffix = {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th") - - return str(day) + suffix + " " + dt.strftime("%b %Y") + suffix = ( + "th" if 11 <= day <= 13 else {1: "st", 2: "nd", 3: "rd"}.get(day % 10, "th") + ) + return f"{day}{suffix} {dt.strftime('%b %Y')}" def generate_card_for_player_summary(player_data): """Generate HTML content based on Steam Player Summary Data""" if not player_data: return None - summary_data = player_data["response"]["players"][0] - personaname = summary_data["personaname"] - personastate = summary_data["personastate"] - avatarfull = summary_data["avatarfull"] - loccountrycode = summary_data.get("loccountrycode", "") - lastlogoff = summary_data["lastlogoff"] - timecreated = summary_data["timecreated"] - gameextrainfo = summary_data.get("gameextrainfo", None) - - # Convert lastlogoff & timecreated from Unix time to human-readable format - lastlogoff_str = format_unix_time(lastlogoff) - timecreated_str = format_unix_time(timecreated) - - personastate_value = personastate_map.get(personastate, "Unknown") - - # Create country section only if loccountrycode exists + + summary = player_data["response"]["players"][0] + personaname = summary.get("personaname", "Unknown") + personastate = personastate_map.get(summary.get("personastate", 0), "Unknown") + avatarfull = summary.get("avatarfull", "") + loccountrycode = summary.get("loccountrycode", "") + lastlogoff = format_unix_time(summary.get("lastlogoff", 0)) + timecreated = format_unix_time(summary.get("timecreated", 0)) + gameextrainfo = summary.get("gameextrainfo") + country_section = "" if loccountrycode: country_section = f""" @@ -150,6 +194,14 @@ def generate_card_for_player_summary(player_data):
""" + game_section = ( + f""" +Currently Playing: {gameextrainfo}
+ """ + if gameextrainfo + else "" + ) + html_content = f""" @@ -187,11 +239,8 @@ def generate_card_for_player_summary(player_data): justify-content: space-between; margin-top: 10px; }} - .info-left {{ - width: 45%; - }} - .info-right {{ - width: 55%; + .info-left, .info-right {{ + width: 48%; }} @@ -203,38 +252,29 @@ def generate_card_for_player_summary(player_data):Status: {personastate_value}
+Status: {personastate}
{country_section}Last Logoff: {lastlogoff_str}
-PC Gaming Since: {timecreated_str}
+Last Logoff: {lastlogoff}
+PC Gaming Since: {timecreated}
Currently Playing: " + - gameextrainfo + "
" if gameextrainfo else ""} + {game_section}