Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add-on store: Improve refreshing the cache #15071

Merged
merged 18 commits into from
Jul 6, 2023
Merged
47 changes: 31 additions & 16 deletions source/_addonStore/dataManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# Can be removed in a future version of python (3.8+)
from __future__ import annotations

from datetime import datetime, timedelta
import json
import os
import pathlib
Expand Down Expand Up @@ -42,6 +41,7 @@
from .network import (
_getAddonStoreURL,
_getCurrentApiVersionForURL,
_CACHE_HASH_URL,
_LATEST_API_VER,
)

Expand All @@ -68,7 +68,6 @@ def initialize():
class _DataManager:
_cacheLatestFilename: str = "_cachedLatestAddons.json"
_cacheCompatibleFilename: str = "_cachedCompatibleAddons.json"
_cachePeriod = timedelta(hours=6)
_downloadsPendingInstall: Set[Tuple["AddonListItemVM", os.PathLike]] = set()

def __init__(self):
Expand Down Expand Up @@ -107,27 +106,43 @@ def _getLatestAddonsDataForVersion(self, apiVersion: str) -> Optional[bytes]:
return None
return response.content

def _cacheCompatibleAddons(self, addonData: str, fetchTime: datetime):
def _getCacheHash(self) -> Optional[str]:
url = _CACHE_HASH_URL
try:
response = requests.get(url)
except requests.exceptions.RequestException as e:
log.debugWarning(f"Unable to get cache hash: {e}")
return None
if response.status_code != requests.codes.OK:
log.error(
f"Unable to get data from API ({url}),"
f" response ({response.status_code}): {response.content}"
)
return None
cacheHash = response.content.decode().strip('"')
nvdaes marked this conversation as resolved.
Show resolved Hide resolved
return cacheHash

def _cacheCompatibleAddons(self, addonData: str, cacheHash: Optional[str]):
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
if not NVDAState.shouldWriteToDisk():
return
if not addonData:
return
cacheData = {
"cacheDate": fetchTime.isoformat(),
"cacheHash": self._getCacheHash(),
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
"data": addonData,
"cachedLanguage": self._lang,
"nvdaAPIVersion": addonAPIVersion.CURRENT,
}
with open(self._cacheCompatibleFile, 'w') as cacheFile:
json.dump(cacheData, cacheFile, ensure_ascii=False)

def _cacheLatestAddons(self, addonData: str, fetchTime: datetime):
def _cacheLatestAddons(self, addonData: str, cacheHash: Optional[str]):
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
if not NVDAState.shouldWriteToDisk():
return
if not addonData:
return
cacheData = {
"cacheDate": fetchTime.isoformat(),
"cacheHash": self._getCacheHash(),
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
"data": addonData,
"cachedLanguage": self._lang,
"nvdaAPIVersion": _LATEST_API_VER,
Expand All @@ -142,10 +157,10 @@ def _getCachedAddonData(self, cacheFilePath: str) -> Optional[CachedAddonsModel]
cacheData = json.load(cacheFile)
Copy link
Member

@seanbudd seanbudd Jul 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also look at fixing #15077, at the same time as #15071 (comment)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nvdaes, you have asked me to test this PR in #15077 (comment).

Testing from source on latest commit (dfe1b22) with the file from #15077, the issue is still present.

You should add a try/except around the two following lines in _getCachedAddonData:

 		with open(cacheFilePath, 'r') as cacheFile:
			cacheData = json.load(cacheFile)

if not cacheData:
return None
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
fetchTime = datetime.fromisoformat(cacheData["cacheDate"])
cacheHash = cacheData.get("cacheHash")
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
return CachedAddonsModel(
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
cachedAddonData=_createStoreCollectionFromJson(cacheData["data"]),
cachedAt=fetchTime,
cacheHash=cacheHash,
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
cachedLanguage=cacheData["cachedLanguage"],
nvdaAPIVersion=tuple(cacheData["nvdaAPIVersion"]), # loads as list
)
Expand All @@ -157,24 +172,24 @@ def getLatestCompatibleAddons(
self,
onDisplayableError: Optional[DisplayableError.OnDisplayableErrorT] = None,
) -> "AddonGUICollectionT":
cacheHash = self._getCacheHash()
shouldRefreshData = (
not self._compatibleAddonCache
or self._compatibleAddonCache.nvdaAPIVersion != addonAPIVersion.CURRENT
or _DataManager._cachePeriod < (datetime.now() - self._compatibleAddonCache.cachedAt)
or self._compatibleAddonCache.cacheHash != cacheHash
or self._compatibleAddonCache.cachedLanguage != self._lang
)
if shouldRefreshData:
fetchTime = datetime.now()
apiData = self._getLatestAddonsDataForVersion(_getCurrentApiVersionForURL())
if apiData:
decodedApiData = apiData.decode()
self._cacheCompatibleAddons(
addonData=decodedApiData,
fetchTime=fetchTime,
cacheHash=cacheHash,
)
self._compatibleAddonCache = CachedAddonsModel(
cachedAddonData=_createStoreCollectionFromJson(decodedApiData),
cachedAt=fetchTime,
cacheHash=cacheHash,
cachedLanguage=self._lang,
nvdaAPIVersion=addonAPIVersion.CURRENT,
)
Expand All @@ -195,23 +210,23 @@ def getLatestAddons(
self,
onDisplayableError: Optional[DisplayableError.OnDisplayableErrorT] = None,
) -> "AddonGUICollectionT":
cacheHash = self._getCacheHash()
shouldRefreshData = (
not self._latestAddonCache
or _DataManager._cachePeriod < (datetime.now() - self._latestAddonCache.cachedAt)
or self._latestAddonCache.cacheHash != cacheHash
or self._latestAddonCache.cachedLanguage != self._lang
)
if shouldRefreshData:
fetchTime = datetime.now()
apiData = self._getLatestAddonsDataForVersion(_LATEST_API_VER)
if apiData:
decodedApiData = apiData.decode()
self._cacheLatestAddons(
addonData=decodedApiData,
fetchTime=fetchTime,
cacheHash=cacheHash,
)
self._latestAddonCache = CachedAddonsModel(
cachedAddonData=_createStoreCollectionFromJson(decodedApiData),
cachedAt=fetchTime,
cacheHash=cacheHash,
cachedLanguage=self._lang,
nvdaAPIVersion=_LATEST_API_VER,
)
Expand Down
3 changes: 1 addition & 2 deletions source/_addonStore/models/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from __future__ import annotations

import dataclasses
from datetime import datetime
import json
import os
from typing import (
Expand Down Expand Up @@ -203,7 +202,7 @@ def isPendingInstall(self) -> bool:
@dataclasses.dataclass
class CachedAddonsModel:
cachedAddonData: "AddonGUICollectionT"
cachedAt: datetime
cacheHash: Optional[str]
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
cachedLanguage: str
# AddonApiVersionT or the string .network._LATEST_API_VER
nvdaAPIVersion: Union[addonAPIVersion.AddonApiVersionT, str]
Expand Down
1 change: 1 addition & 0 deletions source/_addonStore/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from gui.message import DisplayableError


_CACHE_HASH_URL = "https://www.nvaccess.org/addonStore/cacheHash.json"
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
_LATEST_API_VER = "latest"
"""
A string value used in the add-on store to fetch the latest version of all add-ons,
Expand Down