From fd2a632ec1c81699066fc3556649e3e34abda330 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 2 Jul 2026 19:03:57 -0700 Subject: [PATCH 1/3] 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/3] 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/3] 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