From a148342fb72fadfb41d534e29b8a7bfd272be0b5 Mon Sep 17 00:00:00 2001 From: Danfro Date: Thu, 23 Apr 2026 17:03:55 +0200 Subject: [PATCH 1/3] add refresh button to portfolio page --- qml/PortfolioPage.qml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/qml/PortfolioPage.qml b/qml/PortfolioPage.qml index a233b7b..0229bf2 100644 --- a/qml/PortfolioPage.qml +++ b/qml/PortfolioPage.qml @@ -2,6 +2,7 @@ import QtQuick 2.7 import Lomiri.Components 1.3 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 +import io.thp.pyotherside 1.4 import "datastore.js" as DB import "theme.js" as AppTheme @@ -11,7 +12,7 @@ Page { header: PageHeader { title: "Portfolio" ActionBar { - numberOfSlots: 2 + numberOfSlots: 3 anchors.right: parent.right actions: [ Action { @@ -28,12 +29,38 @@ Page { reloadPortfolios() } } + }, + Action { + iconName: "view-refresh" + text: i18n.tr("Refresh") + onTriggered: { + python.call("cli.start_background_sync", function (result) { + console.log("Started") + }); + } } ] } } + Python { + id: python + + Component.onCompleted: { + addImportPath(Qt.resolvedUrl("../src/")); + importModule("cli", function () { + python.call("cli.start_background_sync", function (result) { + console.log("Started") + }); + }); + } + + onError: function (errorName, errorMessage, traceback) { + console.log("Python Error:", errorName, errorMessage, traceback); + } + } + Flickable { id: flick anchors.fill: parent @@ -79,6 +106,7 @@ Page { // === Holdings Header === Text { text: "Your Holdings" + " Worth ($" + totalValue.toFixed(2)+")" + color:AppTheme.getThemeColors(theme.name).textColorPrimary font.bold: true font.pixelSize: units.gu(2.2) } From ef7bd46179444a3af4cdf045cf79e71d58b7a4ed Mon Sep 17 00:00:00 2001 From: Danfro Date: Sat, 25 Apr 2026 19:07:18 +0200 Subject: [PATCH 2/3] try to cover for rate limit error or better avoid it --- src/api_client.py | 128 +++++++++++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 35 deletions(-) diff --git a/src/api_client.py b/src/api_client.py index cf2686e..8c81ad6 100644 --- a/src/api_client.py +++ b/src/api_client.py @@ -75,44 +75,102 @@ def get_coins(limit=1000): pages = (limit + per_page - 1) // per_page results = [] + # Rate-limiting / retry config + max_retries = 3 + min_wait_on_429 = 60 # wait at least 60s when a 429 is received + calls_per_minute = 10 # target pacing inside the allowed limit of 5 - 15 calls per minute + min_interval = 60.0 / calls_per_minute + + last_request_time = 0.0 + for page in range(1, pages + 1): # Construct the URL for the current page url = f"{COINGECKO_BASE}/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page={per_page}&page={page}&sparkline=false&price_change_percentage=1h,24h,7d,30d,90d" - #log.debug(f"[API] Fetching page {page}: {url}") - - try: - # Use the requests session for the API call - response = session.get(url, timeout=30) # Increased timeout for data fetches - response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) - - raw_data = response.content # Get raw bytes - #log.debug(f"Raw response for page {page}: {raw_data[:200]}...") # Log first 200 bytes - data = json.loads(raw_data.decode("utf-8")) - - if not isinstance(data, list): - log.error(f"[Error] Unexpected data format for page {page}: {data}") - continue - - for coin in data: - results.append({ - "id": coin.get("id"), - "name": coin.get("name"), - "symbol": coin.get("symbol", "").upper(), - "price": coin.get("current_price"), - "change24h": coin.get("price_change_percentage_24h"), - "image": coin.get("image"), - "json": coin, # Keep the full JSON for debugging or future use - }) - except requests.exceptions.HTTPError as e: - log.error(f"[HTTP Error] Page {page} failed with status {e.response.status_code}: {e.response.text}") - except requests.exceptions.ConnectionError as e: - log.error(f"[Connection Error] Page {page} failed: {e}. Check network and DNS resolution.") - except requests.exceptions.Timeout as e: - log.error(f"[Timeout Error] Page {page} timed out: {e}") - except json.JSONDecodeError as e: - log.error(f"[JSON Decode Error] Page {page} failed to parse JSON: {e}") - except Exception as e: - log.error(f"[Unexpected Error] Page {page} failed: {e}", exc_info=True) # exc_info to get traceback + + retries = 0 + while retries <= max_retries: + # pace requests to respect target rate + elapsed = time.time() - last_request_time + if elapsed < min_interval: + time.sleep(min_interval - elapsed) + + try: + # Use the requests session for the API call + response = session.get(url, timeout=30) # Increased timeout for data fetches + last_request_time = time.time() + + # Explicit handling for rate limiting (429) + if response.status_code == 429: + retry_after = response.headers.get("Retry-After") + if retry_after is not None: + try: + wait = max(min_wait_on_429, int(retry_after)) + except ValueError: + wait = min_wait_on_429 + else: + wait = min_wait_on_429 + + log.warning(f"[Rate Limit] Page {page} 429 received. Waiting {wait}s before retry #{retries+1}.") + time.sleep(wait) + retries += 1 + continue + + response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) + + raw_data = response.content # Get raw bytes + data = json.loads(raw_data.decode("utf-8")) + + if not isinstance(data, list): + log.error(f"[Error] Unexpected data format for page {page}: {data}") + break # skip this page and continue with next + + for coin in data: + results.append({ + "id": coin.get("id"), + "name": coin.get("name"), + "symbol": coin.get("symbol", "").upper(), + "price": coin.get("current_price"), + "change24h": coin.get("price_change_percentage_24h"), + "image": coin.get("image"), + "json": coin, # Keep the full JSON for debugging or future use + }) + break # success -> exit retry loop + + except requests.exceptions.HTTPError as e: + status = e.response.status_code if e.response is not None else None + if status == 429: + retry_after = e.response.headers.get("Retry-After") if e.response is not None else None + try: + wait = max(min_wait_on_429, int(retry_after)) if retry_after else min_wait_on_429 + except ValueError: + wait = min_wait_on_429 + log.warning(f"[HTTPError 429] Page {page}: waiting {wait}s before retry #{retries+1}.") + time.sleep(wait) + retries += 1 + continue + + log.error(f"[HTTP Error] Page {page} failed with status {status}: {e.response.text if e.response is not None else e}") + break + + except requests.exceptions.ConnectionError as e: + log.error(f"[Connection Error] Page {page} failed: {e}. Check network and DNS resolution.") + break + + except requests.exceptions.Timeout as e: + log.error(f"[Timeout Error] Page {page} timed out: {e}") + break + + except json.JSONDecodeError as e: + log.error(f"[JSON Decode Error] Page {page} failed to parse JSON: {e}") + break + + except Exception as e: + log.error(f"[Unexpected Error] Page {page} failed: {e}", exc_info=True) # exc_info to get traceback + break + + else: + # executed if while loop exhausted without break (i.e., retries exceeded) + log.error(f"[Rate Limit] Page {page} exhausted retries ({max_retries}). Skipping page.") # Return only up to the specified limit return results[:limit] From 49945247688ebabfa426515d6dc822095c257dff Mon Sep 17 00:00:00 2001 From: Danfro Date: Sat, 25 Apr 2026 23:42:16 +0200 Subject: [PATCH 3/3] drop complex python usage for refreshing, use existing function --- qml/PortfolioPage.qml | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/qml/PortfolioPage.qml b/qml/PortfolioPage.qml index 0229bf2..0224e51 100644 --- a/qml/PortfolioPage.qml +++ b/qml/PortfolioPage.qml @@ -2,7 +2,6 @@ import QtQuick 2.7 import Lomiri.Components 1.3 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 -import io.thp.pyotherside 1.4 import "datastore.js" as DB import "theme.js" as AppTheme @@ -33,34 +32,12 @@ Page { Action { iconName: "view-refresh" text: i18n.tr("Refresh") - onTriggered: { - python.call("cli.start_background_sync", function (result) { - console.log("Started") - }); - } + onTriggered: reloadPortfolios() } - ] } } - Python { - id: python - - Component.onCompleted: { - addImportPath(Qt.resolvedUrl("../src/")); - importModule("cli", function () { - python.call("cli.start_background_sync", function (result) { - console.log("Started") - }); - }); - } - - onError: function (errorName, errorMessage, traceback) { - console.log("Python Error:", errorName, errorMessage, traceback); - } - } - Flickable { id: flick anchors.fill: parent