From fd2a632ec1c81699066fc3556649e3e34abda330 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 2 Jul 2026 19:03:57 -0700 Subject: [PATCH 1/8] feat: store api in cache --- src/py/mat3ra/notebooks_utils/auth.py | 11 ++++- src/py/mat3ra/notebooks_utils/token_store.py | 49 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/py/mat3ra/notebooks_utils/token_store.py diff --git a/src/py/mat3ra/notebooks_utils/auth.py b/src/py/mat3ra/notebooks_utils/auth.py index b8c3cb634..aad2fbeb0 100644 --- a/src/py/mat3ra/notebooks_utils/auth.py +++ b/src/py/mat3ra/notebooks_utils/auth.py @@ -3,11 +3,12 @@ from mat3ra.api_client import ACCESS_TOKEN_ENV_VAR -from .core.api.auth import authenticate_oidc +from .core.api.auth import authenticate_oidc, get_oidc_base_url, store_token_data_in_environment from .io import get_data from .ipython.ui import show_device_flow_popup from .primitive.environment import ENVIRONMENT, EnvironmentsEnum from .pyodide.api.auth import authenticate_jupyterlite +from .token_store import load_token, save_token REFRESH_TOKEN_ENV_VAR = "OIDC_REFRESH_TOKEN" @@ -25,4 +26,10 @@ async def authenticate(force=False, globals_dict=None): if data_from_host: await authenticate_jupyterlite(data_from_host) elif ACCESS_TOKEN_ENV_VAR not in os.environ or force: - await authenticate_oidc(show_popup=show_device_flow_popup) + oidc_url = get_oidc_base_url() + cached = not force and load_token(oidc_url) + if cached: + store_token_data_in_environment(cached) + else: + token_data = await authenticate_oidc(show_popup=show_device_flow_popup) + save_token(oidc_url, token_data) diff --git a/src/py/mat3ra/notebooks_utils/token_store.py b/src/py/mat3ra/notebooks_utils/token_store.py new file mode 100644 index 000000000..25de358b7 --- /dev/null +++ b/src/py/mat3ra/notebooks_utils/token_store.py @@ -0,0 +1,49 @@ +"""Persist OIDC tokens across notebook kernels (localStorage or file).""" + +import json +import os +import time + +from .primitive.environment import is_pyodide_environment + +_USE_BROWSER = is_pyodide_environment() +if _USE_BROWSER: + import js # type: ignore + +_FILE_PATH = os.path.join(os.path.expanduser("~"), ".mat3ra", "oidc_token_cache.json") +_LOCAL_STORAGE_KEY = "mat3ra_oidc_token_cache" +_EXPIRY_BUFFER = 60 # seconds before actual expiry to consider token stale + + +def save_token(oidc_url: str, token_data: dict) -> None: + token_data["expires_at"] = time.time() + token_data.get("expires_in", 3600) + cache = _read_cache() + cache[oidc_url] = token_data + _write_cache(cache) + + +def load_token(oidc_url: str): + data = _read_cache().get(oidc_url) + if data and data.get("expires_at", 0) > time.time() + _EXPIRY_BUFFER: + return data + return None + + +def _read_cache() -> dict: + if _USE_BROWSER: + return json.loads(js.localStorage.getItem(_LOCAL_STORAGE_KEY) or "{}") + try: + with open(_FILE_PATH) as f: + return json.load(f) + except Exception: + return {} + + +def _write_cache(cache: dict) -> None: + if _USE_BROWSER: + js.localStorage.setItem(_LOCAL_STORAGE_KEY, json.dumps(cache)) + else: + os.makedirs(os.path.dirname(_FILE_PATH), mode=0o700, exist_ok=True) + with open(_FILE_PATH, "w") as f: + json.dump(cache, f) + os.chmod(_FILE_PATH, 0o600) From 9574450f4da6589a6ab393e1331303279fe54e63 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 2 Jul 2026 20:42:01 -0700 Subject: [PATCH 2/8] update: use index db for pyodide --- src/py/mat3ra/notebooks_utils/auth.py | 4 +- .../notebooks_utils/core/api/token_store.py | 21 ++++++++ .../pyodide/api/token_store.py | 45 ++++++++++++++++ src/py/mat3ra/notebooks_utils/token_store.py | 54 +++++++------------ 4 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 src/py/mat3ra/notebooks_utils/core/api/token_store.py create mode 100644 src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py diff --git a/src/py/mat3ra/notebooks_utils/auth.py b/src/py/mat3ra/notebooks_utils/auth.py index aad2fbeb0..239b34afb 100644 --- a/src/py/mat3ra/notebooks_utils/auth.py +++ b/src/py/mat3ra/notebooks_utils/auth.py @@ -27,9 +27,9 @@ async def authenticate(force=False, globals_dict=None): await authenticate_jupyterlite(data_from_host) elif ACCESS_TOKEN_ENV_VAR not in os.environ or force: oidc_url = get_oidc_base_url() - cached = not force and load_token(oidc_url) + cached = None if force else await load_token(oidc_url) if cached: store_token_data_in_environment(cached) else: token_data = await authenticate_oidc(show_popup=show_device_flow_popup) - save_token(oidc_url, token_data) + await save_token(oidc_url, token_data) diff --git a/src/py/mat3ra/notebooks_utils/core/api/token_store.py b/src/py/mat3ra/notebooks_utils/core/api/token_store.py new file mode 100644 index 000000000..341ea6bf0 --- /dev/null +++ b/src/py/mat3ra/notebooks_utils/core/api/token_store.py @@ -0,0 +1,21 @@ +"""File-based OIDC token cache (~/.mat3ra/oidc_token_cache.json, 0o600).""" + +import json +import os + +_PATH = os.path.join(os.path.expanduser("~"), ".mat3ra", "oidc_token_cache.json") + + +async def read() -> dict: + try: + with open(_PATH) as f: + return json.load(f) + except Exception: + return {} + + +async def write(cache: dict) -> None: + os.makedirs(os.path.dirname(_PATH), mode=0o700, exist_ok=True) + with open(_PATH, "w") as f: + json.dump(cache, f) + os.chmod(_PATH, 0o600) diff --git a/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py b/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py new file mode 100644 index 000000000..2926dafe6 --- /dev/null +++ b/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py @@ -0,0 +1,45 @@ +"""IndexedDB-based OIDC token cache for Pyodide Web Workers.""" + +import json + +from pyodide.code import run_js # type: ignore + +_idb = run_js( + """ +(function() { + const open = () => new Promise(resolve => { + const req = indexedDB.open("mat3ra", 1); + req.onupgradeneeded = () => req.result.createObjectStore("tokens"); + req.onsuccess = () => resolve(req.result); + req.onerror = () => resolve(null); + }); + return { + get: async () => { + const db = await open(); + if (!db) return null; + return new Promise(resolve => { + const g = db.transaction("tokens").objectStore("tokens").get("oidc_cache"); + g.onsuccess = () => { db.close(); resolve(g.result || null); }; + g.onerror = () => { db.close(); resolve(null); }; + }); + }, + set: async (data) => { + const db = await open(); + if (!db) return; + const tx = db.transaction("tokens", "readwrite"); + tx.objectStore("tokens").put(data, "oidc_cache"); + return new Promise(resolve => { tx.oncomplete = () => { db.close(); resolve(); }; }); + } + }; +})() +""" +) + + +async def read() -> dict: + result = await _idb.get() + return json.loads(str(result)) if result else {} + + +async def write(cache: dict) -> None: + await _idb.set(json.dumps(cache)) diff --git a/src/py/mat3ra/notebooks_utils/token_store.py b/src/py/mat3ra/notebooks_utils/token_store.py index 25de358b7..4cd6e6e6b 100644 --- a/src/py/mat3ra/notebooks_utils/token_store.py +++ b/src/py/mat3ra/notebooks_utils/token_store.py @@ -1,49 +1,35 @@ -"""Persist OIDC tokens across notebook kernels (localStorage or file).""" +"""Persist OIDC tokens across notebook kernels.""" -import json -import os import time +from typing import Optional from .primitive.environment import is_pyodide_environment -_USE_BROWSER = is_pyodide_environment() -if _USE_BROWSER: - import js # type: ignore +if is_pyodide_environment(): + from .pyodide.api import token_store as _storage +else: + from .core.api import token_store as _storage # type: ignore[no-redef] -_FILE_PATH = os.path.join(os.path.expanduser("~"), ".mat3ra", "oidc_token_cache.json") -_LOCAL_STORAGE_KEY = "mat3ra_oidc_token_cache" _EXPIRY_BUFFER = 60 # seconds before actual expiry to consider token stale -def save_token(oidc_url: str, token_data: dict) -> None: - token_data["expires_at"] = time.time() + token_data.get("expires_in", 3600) - cache = _read_cache() - cache[oidc_url] = token_data - _write_cache(cache) +async def save_token(oidc_url: str, token_data: dict) -> None: + token_cache_entry = dict(token_data) + token_cache_entry["expires_at"] = time.time() + token_cache_entry.get("expires_in", 3600) + token_cache = await _storage.read() + token_cache[oidc_url] = token_cache_entry + await _storage.write(token_cache) -def load_token(oidc_url: str): - data = _read_cache().get(oidc_url) - if data and data.get("expires_at", 0) > time.time() + _EXPIRY_BUFFER: - return data - return None +async def load_token(oidc_url: str) -> Optional[dict]: + token_cache_entry = (await _storage.read()).get(oidc_url) -def _read_cache() -> dict: - if _USE_BROWSER: - return json.loads(js.localStorage.getItem(_LOCAL_STORAGE_KEY) or "{}") - try: - with open(_FILE_PATH) as f: - return json.load(f) - except Exception: - return {} + if not token_cache_entry: + return None + expires_at = token_cache_entry.get("expires_at", 0) + if expires_at <= time.time() + _EXPIRY_BUFFER: + return None -def _write_cache(cache: dict) -> None: - if _USE_BROWSER: - js.localStorage.setItem(_LOCAL_STORAGE_KEY, json.dumps(cache)) - else: - os.makedirs(os.path.dirname(_FILE_PATH), mode=0o700, exist_ok=True) - with open(_FILE_PATH, "w") as f: - json.dump(cache, f) - os.chmod(_FILE_PATH, 0o600) + return token_cache_entry From e1db3ac7a024bb9a6438c31ec7740bef966d8339 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 2 Jul 2026 20:49:52 -0700 Subject: [PATCH 3/8] update move files --- .../notebooks_utils/core/api/token_store.py | 42 ++++++++++++++++--- src/py/mat3ra/notebooks_utils/token_store.py | 35 +++------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/py/mat3ra/notebooks_utils/core/api/token_store.py b/src/py/mat3ra/notebooks_utils/core/api/token_store.py index 341ea6bf0..0d4834d1d 100644 --- a/src/py/mat3ra/notebooks_utils/core/api/token_store.py +++ b/src/py/mat3ra/notebooks_utils/core/api/token_store.py @@ -1,21 +1,51 @@ -"""File-based OIDC token cache (~/.mat3ra/oidc_token_cache.json, 0o600).""" +"""OIDC token cache — business logic + file-based storage (default).""" import json import os +import sys +import time +from typing import Optional -_PATH = os.path.join(os.path.expanduser("~"), ".mat3ra", "oidc_token_cache.json") +_EXPIRY_BUFFER = 60 # seconds before actual expiry to consider token stale +_FILE_PATH = os.path.join(os.path.expanduser("~"), ".mat3ra", "oidc_token_cache.json") + +# Storage backend: defaults to this module (file-based). +# Overridden by top-level token_store.py for Pyodide (IndexedDB). +_storage = sys.modules[__name__] async def read() -> dict: try: - with open(_PATH) as f: + with open(_FILE_PATH) as f: return json.load(f) except Exception: return {} async def write(cache: dict) -> None: - os.makedirs(os.path.dirname(_PATH), mode=0o700, exist_ok=True) - with open(_PATH, "w") as f: + os.makedirs(os.path.dirname(_FILE_PATH), mode=0o700, exist_ok=True) + with open(_FILE_PATH, "w") as f: json.dump(cache, f) - os.chmod(_PATH, 0o600) + os.chmod(_FILE_PATH, 0o600) + + +async def save_token(oidc_url: str, token_data: dict) -> None: + token_cache_entry = dict(token_data) + token_cache_entry["expires_at"] = time.time() + token_cache_entry.get("expires_in", 3600) + + token_cache = await _storage.read() + token_cache[oidc_url] = token_cache_entry + await _storage.write(token_cache) + + +async def load_token(oidc_url: str) -> Optional[dict]: + token_cache_entry = (await _storage.read()).get(oidc_url) + + if not token_cache_entry: + return None + + expires_at = token_cache_entry.get("expires_at", 0) + if expires_at <= time.time() + _EXPIRY_BUFFER: + return None + + return token_cache_entry diff --git a/src/py/mat3ra/notebooks_utils/token_store.py b/src/py/mat3ra/notebooks_utils/token_store.py index 4cd6e6e6b..f2efec5c4 100644 --- a/src/py/mat3ra/notebooks_utils/token_store.py +++ b/src/py/mat3ra/notebooks_utils/token_store.py @@ -1,35 +1,10 @@ -"""Persist OIDC tokens across notebook kernels.""" - -import time -from typing import Optional +"""Persist OIDC tokens across notebook kernels — thin routing adapter.""" +from .core.api.token_store import load_token, save_token # noqa: F401 — re-export from .primitive.environment import is_pyodide_environment if is_pyodide_environment(): - from .pyodide.api import token_store as _storage -else: - from .core.api import token_store as _storage # type: ignore[no-redef] - -_EXPIRY_BUFFER = 60 # seconds before actual expiry to consider token stale - - -async def save_token(oidc_url: str, token_data: dict) -> None: - token_cache_entry = dict(token_data) - token_cache_entry["expires_at"] = time.time() + token_cache_entry.get("expires_in", 3600) - - token_cache = await _storage.read() - token_cache[oidc_url] = token_cache_entry - await _storage.write(token_cache) - - -async def load_token(oidc_url: str) -> Optional[dict]: - token_cache_entry = (await _storage.read()).get(oidc_url) - - if not token_cache_entry: - return None - - expires_at = token_cache_entry.get("expires_at", 0) - if expires_at <= time.time() + _EXPIRY_BUFFER: - return None + from .core.api import token_store as _core_ts + from .pyodide.api import token_store as _pyodide_storage - return token_cache_entry + _core_ts._storage = _pyodide_storage From cb13e96581536d347bd339272695df455290f79b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 6 Jul 2026 11:01:03 -0700 Subject: [PATCH 4/8] update: streamline --- .../notebooks_utils/core/api/token_store.py | 42 ++----------------- .../pyodide/api/token_store.py | 2 +- src/py/mat3ra/notebooks_utils/token_store.py | 33 ++++++++++++--- 3 files changed, 33 insertions(+), 44 deletions(-) diff --git a/src/py/mat3ra/notebooks_utils/core/api/token_store.py b/src/py/mat3ra/notebooks_utils/core/api/token_store.py index 0d4834d1d..609f02933 100644 --- a/src/py/mat3ra/notebooks_utils/core/api/token_store.py +++ b/src/py/mat3ra/notebooks_utils/core/api/token_store.py @@ -1,51 +1,17 @@ -"""OIDC token cache — business logic + file-based storage (default).""" +"""File-based OIDC token cache storage.""" import json import os -import sys -import time -from typing import Optional -_EXPIRY_BUFFER = 60 # seconds before actual expiry to consider token stale _FILE_PATH = os.path.join(os.path.expanduser("~"), ".mat3ra", "oidc_token_cache.json") -# Storage backend: defaults to this module (file-based). -# Overridden by top-level token_store.py for Pyodide (IndexedDB). -_storage = sys.modules[__name__] - async def read() -> dict: - try: - with open(_FILE_PATH) as f: - return json.load(f) - except Exception: - return {} + with open(_FILE_PATH) as f: + return json.load(f) async def write(cache: dict) -> None: - os.makedirs(os.path.dirname(_FILE_PATH), mode=0o700, exist_ok=True) + os.makedirs(os.path.dirname(_FILE_PATH), exist_ok=True) with open(_FILE_PATH, "w") as f: json.dump(cache, f) - os.chmod(_FILE_PATH, 0o600) - - -async def save_token(oidc_url: str, token_data: dict) -> None: - token_cache_entry = dict(token_data) - token_cache_entry["expires_at"] = time.time() + token_cache_entry.get("expires_in", 3600) - - token_cache = await _storage.read() - token_cache[oidc_url] = token_cache_entry - await _storage.write(token_cache) - - -async def load_token(oidc_url: str) -> Optional[dict]: - token_cache_entry = (await _storage.read()).get(oidc_url) - - if not token_cache_entry: - return None - - expires_at = token_cache_entry.get("expires_at", 0) - if expires_at <= time.time() + _EXPIRY_BUFFER: - return None - - return token_cache_entry diff --git a/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py b/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py index 2926dafe6..7e5414a5b 100644 --- a/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py +++ b/src/py/mat3ra/notebooks_utils/pyodide/api/token_store.py @@ -1,4 +1,4 @@ -"""IndexedDB-based OIDC token cache for Pyodide Web Workers.""" +"""IndexedDB-based OIDC token cache storage for Pyodide Web Workers.""" import json diff --git a/src/py/mat3ra/notebooks_utils/token_store.py b/src/py/mat3ra/notebooks_utils/token_store.py index f2efec5c4..73eebd6a8 100644 --- a/src/py/mat3ra/notebooks_utils/token_store.py +++ b/src/py/mat3ra/notebooks_utils/token_store.py @@ -1,10 +1,33 @@ -"""Persist OIDC tokens across notebook kernels — thin routing adapter.""" +"""Persist OIDC tokens across notebook kernels. + +Switches storage between file (Python) and IndexedDB (Pyodide). +""" + +import time +from typing import Optional -from .core.api.token_store import load_token, save_token # noqa: F401 — re-export from .primitive.environment import is_pyodide_environment if is_pyodide_environment(): - from .core.api import token_store as _core_ts - from .pyodide.api import token_store as _pyodide_storage + from .pyodide.api.token_store import read as _read + from .pyodide.api.token_store import write as _write +else: + from .core.api.token_store import read as _read + from .core.api.token_store import write as _write + +_EXPIRY_BUFFER = 60 * 5 # to avoid using soon-to-expire tokens + + +async def save_token(oidc_url: str, token_data: dict) -> None: + entry = dict(token_data) + entry["expires_at"] = time.time() + entry.get("expires_in", 3600) + cache = await _read() + cache[oidc_url] = entry + await _write(cache) + - _core_ts._storage = _pyodide_storage +async def load_token(oidc_url: str) -> Optional[dict]: + entry = (await _read()).get(oidc_url) + if not entry or entry.get("expires_at", 0) <= time.time() + _EXPIRY_BUFFER: + return None + return entry From b1803b181fd2b14001fdb236b0bc51cb45788168 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 6 Jul 2026 11:07:24 -0700 Subject: [PATCH 5/8] update: split to simplify --- src/py/mat3ra/notebooks_utils/auth.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/py/mat3ra/notebooks_utils/auth.py b/src/py/mat3ra/notebooks_utils/auth.py index 239b34afb..65f07926b 100644 --- a/src/py/mat3ra/notebooks_utils/auth.py +++ b/src/py/mat3ra/notebooks_utils/auth.py @@ -13,6 +13,18 @@ REFRESH_TOKEN_ENV_VAR = "OIDC_REFRESH_TOKEN" +async def _authenticate_oidc_with_cache(force=False): + oidc_url = get_oidc_base_url() + cached = None if force else await load_token(oidc_url) + + if cached: + store_token_data_in_environment(cached) + return + + token_data = await authenticate_oidc(show_popup=show_device_flow_popup) + await save_token(oidc_url, token_data) + + async def authenticate(force=False, globals_dict=None): if globals_dict is None: frame = inspect.currentframe() @@ -20,16 +32,13 @@ async def authenticate(force=False, globals_dict=None): globals_dict = frame.f_back.f_globals # type: ignore finally: del frame + if ENVIRONMENT == EnvironmentsEnum.PYODIDE: get_data("data_from_host", globals_dict) + data_from_host = globals_dict.get("data_from_host") + if data_from_host: await authenticate_jupyterlite(data_from_host) elif ACCESS_TOKEN_ENV_VAR not in os.environ or force: - oidc_url = get_oidc_base_url() - cached = None if force else await load_token(oidc_url) - if cached: - store_token_data_in_environment(cached) - else: - token_data = await authenticate_oidc(show_popup=show_device_flow_popup) - await save_token(oidc_url, token_data) + await _authenticate_oidc_with_cache(force) From 13cd6ceb971e6e859c9905f3ba0950fe4f703d36 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 6 Jul 2026 11:20:23 -0700 Subject: [PATCH 6/8] chore: explain --- src/py/mat3ra/notebooks_utils/auth.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/notebooks_utils/auth.py b/src/py/mat3ra/notebooks_utils/auth.py index 65f07926b..433f36490 100644 --- a/src/py/mat3ra/notebooks_utils/auth.py +++ b/src/py/mat3ra/notebooks_utils/auth.py @@ -1,3 +1,15 @@ +"""Notebook authentication. + +Two paths depending on environment: + +1. JupyterLite embedded in WebApp — the host app injects a token via + postMessage and authenticate_jupyterlite() stores it in the environment. + +2. Standalone / IDE / local dev — OIDC device-code flow. Tokens are cached + (file on disk or IndexedDB in Pyodide) so the user only logs in once + per expiry window. +""" + import inspect import os @@ -6,7 +18,7 @@ from .core.api.auth import authenticate_oidc, get_oidc_base_url, store_token_data_in_environment from .io import get_data from .ipython.ui import show_device_flow_popup -from .primitive.environment import ENVIRONMENT, EnvironmentsEnum +from .primitive.environment import is_pyodide_environment from .pyodide.api.auth import authenticate_jupyterlite from .token_store import load_token, save_token @@ -26,6 +38,15 @@ async def _authenticate_oidc_with_cache(force=False): async def authenticate(force=False, globals_dict=None): + """ + Authenticate the current notebook session with the Mat3ra platform. + Follow the link to a new browser window to complete the authentication process. + The token is cached in the environment for future use. + + Args: + force: Re-authenticate even if a valid cached token exists. + globals_dict: Caller's global namespace. Auto-detected if not provided. + """ if globals_dict is None: frame = inspect.currentframe() try: @@ -33,7 +54,7 @@ async def authenticate(force=False, globals_dict=None): finally: del frame - if ENVIRONMENT == EnvironmentsEnum.PYODIDE: + if is_pyodide_environment(): get_data("data_from_host", globals_dict) data_from_host = globals_dict.get("data_from_host") From 54391014da75909dd34f687cf2be534c60bf0ae6 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 8 Jul 2026 16:26:34 -0700 Subject: [PATCH 7/8] chore: defensive read --- src/py/mat3ra/notebooks_utils/core/api/token_store.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py/mat3ra/notebooks_utils/core/api/token_store.py b/src/py/mat3ra/notebooks_utils/core/api/token_store.py index 609f02933..0280fc1c7 100644 --- a/src/py/mat3ra/notebooks_utils/core/api/token_store.py +++ b/src/py/mat3ra/notebooks_utils/core/api/token_store.py @@ -7,8 +7,11 @@ async def read() -> dict: - with open(_FILE_PATH) as f: - return json.load(f) + try: + with open(_FILE_PATH) as f: + return json.load(f) + except FileNotFoundError: + return {} async def write(cache: dict) -> None: From cb52523f35a3c8acdff58c42f03a4da98d01ee12 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 8 Jul 2026 16:30:49 -0700 Subject: [PATCH 8/8] chore: defensive read --- src/py/mat3ra/notebooks_utils/core/api/token_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/mat3ra/notebooks_utils/core/api/token_store.py b/src/py/mat3ra/notebooks_utils/core/api/token_store.py index 0280fc1c7..17b9f8725 100644 --- a/src/py/mat3ra/notebooks_utils/core/api/token_store.py +++ b/src/py/mat3ra/notebooks_utils/core/api/token_store.py @@ -10,7 +10,7 @@ async def read() -> dict: try: with open(_FILE_PATH) as f: return json.load(f) - except FileNotFoundError: + except (FileNotFoundError, json.JSONDecodeError): return {}