From 066d1ff2a4321f34987b4c7a22d19ea6e23547b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Thu, 15 Jan 2026 12:04:50 +0000 Subject: [PATCH] Test --- .../bahmcloud_store/providers.py | 97 +++++++++++++++++-- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/custom_components/bahmcloud_store/providers.py b/custom_components/bahmcloud_store/providers.py index cd56c95..dad6f1a 100644 --- a/custom_components/bahmcloud_store/providers.py +++ b/custom_components/bahmcloud_store/providers.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging from dataclasses import dataclass -from urllib.parse import urlparse +from urllib.parse import quote_plus, urlparse from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -45,27 +45,38 @@ def _split_owner_repo(repo_url: str) -> tuple[str | None, str | None]: def detect_provider(repo_url: str) -> str: host = urlparse(repo_url).netloc.lower() + # Keep explicit providers stable if "github.com" in host: return "github" - if "gitlab.com" in host: + if "gitlab" in host: # covers gitlab.com and many self-hosted domains containing "gitlab" return "gitlab" + + # Heuristic: if it looks like a standard // URL, treat as gitea-like owner, repo = _split_owner_repo(repo_url) if owner and repo: return "gitea" + return "generic" -async def _safe_json(session, url: str, *, headers: dict | None = None, timeout: int = 15): +async def _safe_json(session, url: str, *, headers: dict | None = None, timeout: int = 20): try: async with session.get(url, timeout=timeout, headers=headers) as resp: - if resp.status != 200: - return None, resp.status - return await resp.json(), resp.status + status = resp.status + if status != 200: + return None, status + return await resp.json(), status except Exception: return None, None async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) -> tuple[str | None, str | None]: + """ + Robust strategy: + 1) /releases/latest (can be 404 if no releases) + 2) /releases?per_page=1 (some repos have releases but latest endpoint can still behave differently) + 3) /tags?per_page=1 + """ session = async_get_clientsession(hass) headers = { "Accept": "application/vnd.github+json", @@ -73,13 +84,24 @@ async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) -> } # 1) Releases latest - data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers) + data, status = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers) if isinstance(data, dict): tag = data.get("tag_name") or data.get("name") if isinstance(tag, str) and tag.strip(): return tag.strip(), "release" - # 2) Fallback: newest tag + # If rate-limited, log and continue with tags (tags may also be blocked but try) + if status == 403: + _LOGGER.debug("GitHub rate limit or forbidden on releases/latest for %s/%s", owner, repo) + + # 2) Releases list fallback + data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases?per_page=1", headers=headers) + if isinstance(data, list) and data: + tag = data[0].get("tag_name") or data[0].get("name") + if isinstance(tag, str) and tag.strip(): + return tag.strip(), "release" + + # 3) Tag fallback data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/tags?per_page=1", headers=headers) if isinstance(data, list) and data: tag = data[0].get("name") @@ -107,6 +129,42 @@ async def _gitea_latest_version(hass: HomeAssistant, base: str, owner: str, repo return None, None +async def _gitlab_latest_version(hass: HomeAssistant, base: str, owner: str, repo: str) -> tuple[str | None, str | None]: + """ + GitLab API: + - releases: /api/v4/projects/:id/releases + - tags: /api/v4/projects/:id/repository/tags + Project id can be URL-encoded full path (owner/repo). + """ + session = async_get_clientsession(hass) + headers = {"User-Agent": UA} + project = quote_plus(f"{owner}/{repo}") + + # 1) Releases (newest first) + data, _ = await _safe_json( + session, + f"{base}/api/v4/projects/{project}/releases?per_page=1&order_by=released_at&sort=desc", + headers=headers, + ) + if isinstance(data, list) and data: + tag = data[0].get("tag_name") or data[0].get("name") + if isinstance(tag, str) and tag.strip(): + return tag.strip(), "release" + + # 2) Tags (newest first) + data, _ = await _safe_json( + session, + f"{base}/api/v4/projects/{project}/repository/tags?per_page=1&order_by=updated&sort=desc", + headers=headers, + ) + if isinstance(data, list) and data: + tag = data[0].get("name") + if isinstance(tag, str) and tag.strip(): + return tag.strip(), "tag" + + return None, None + + async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo: provider = detect_provider(repo_url) owner, repo = _split_owner_repo(repo_url) @@ -146,6 +204,27 @@ async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo: info.latest_version_source = src return info + if provider == "gitlab": + u = urlparse(repo_url.rstrip("/")) + base = f"{u.scheme}://{u.netloc}" + headers = {"User-Agent": UA} + project = quote_plus(f"{owner}/{repo}") + + # Project info + data, _ = await _safe_json(session, f"{base}/api/v4/projects/{project}", headers=headers) + if isinstance(data, dict): + info.description = data.get("description") + info.repo_name = _normalize_repo_name(data.get("path")) or repo + info.default_branch = data.get("default_branch") or "main" + ns = data.get("namespace") + if isinstance(ns, dict) and ns.get("path"): + info.owner = ns.get("path") + + ver, src = await _gitlab_latest_version(hass, base, owner, repo) + info.latest_version = ver + info.latest_version_source = src + return info + if provider == "gitea": u = urlparse(repo_url.rstrip("/")) base = f"{u.scheme}://{u.netloc}" @@ -167,4 +246,4 @@ async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo: except Exception as e: _LOGGER.debug("Provider fetch failed for %s: %s", repo_url, e) - return info + return info \ No newline at end of file