a-jupyterlite-session
Version:
A JupyterLite extension.
198 lines (197 loc) • 9 kB
JavaScript
import { PathExt } from '@jupyterlab/coreutils';
import { IDefaultDrive } from '@jupyterlab/services';
import { v4 as uuidv4 } from 'uuid';
import { IJupyterLiteSession } from './tokens';
/**
* Initialization data for the a-jupyterlite-session extension.
*/
const plugin = {
id: 'a-jupyterlite-session:plugin',
autoStart: true,
provides: IJupyterLiteSession,
requires: [IDefaultDrive],
activate: async (_app, idrive) => {
const SESSIONS = '.sessions';
const README = 'README.md';
const REQUIREMENTS = 'requirements.txt';
const LOCKFILE = 'pyodideKernelLockFileURL';
const IGNORE_PACKAGES = [
'jupyterlite-cors',
'micropip',
'pyodide-http',
'ipykernel' // JupyterLite provides this package
];
const drive = idrive;
await drive.ready;
console.log('JupyterLite extension a-jupyterlite-session is activated!');
const storage = drive._storage;
const now = new Date();
if (!(await storage.getItem(SESSIONS))) {
await storage.setItem(SESSIONS, {
name: SESSIONS,
path: SESSIONS,
last_modified: now.toISOString(),
created: now.toISOString(),
format: 'json',
mimetype: '',
content: null,
size: 0,
writable: true,
type: 'directory'
});
}
// Generate a '%dd-%mm-%yyyy-%hh-%mm-%ss' timestamp
const timestamp = now
.toLocaleDateString('en-us', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h24'
})
.replace(/\//g, '-')
.replace(/:/g, '-')
.replace(', ', '-');
const salt = uuidv4().slice(0, 8);
// Generate a unique session directory name with the current time and a
// random suffix
const sessionPath = PathExt.join(SESSIONS, `${timestamp}-${salt}`);
// Create the new directory
await storage.setItem(sessionPath, {
name: PathExt.basename(sessionPath),
path: sessionPath,
last_modified: now.toISOString(),
created: now.toISOString(),
format: 'json',
mimetype: '',
content: null,
size: 0,
writable: true,
type: 'directory'
});
// If litegitpuller is used to load a repo, set its uploadpath to the
// session folder
const url = new URL(window.location.href);
if (url.searchParams.get('repo') !== null) {
url.searchParams.set('uploadpath', sessionPath);
window.history.replaceState(null, '', url);
}
// Copy the current README.md file to the new session folder, without awaiting it
drive
.copy(README, sessionPath)
.catch(reason => console.warn(`Failed to copy the ${README} file to the new session: ${reason}`));
const lockfileUrlParam = new URL(window.location.href).searchParams.get(LOCKFILE);
if (lockfileUrlParam === null) {
// Copy the current requirements.txt file to the new session folder,
// without awaiting it
drive
.copy(REQUIREMENTS, sessionPath)
.catch(reason => console.warn(`Failed to copy the ${REQUIREMENTS} file to the new session: ${reason}`));
}
else {
// Fetch the Pyodide lockfile to dynamically create the requirements.txt
// file in the new session folder
async function createRequirementsFileFromPyodideLockfile() {
var _a, _b, _c, _d;
let lockfileUrl;
try {
// @ts-ignore
lockfileUrl = JSON.parse(lockfileUrlParam);
}
catch (reason) {
console.warn(`Invalid Pyodide lockfile URL: ${reason}`);
return;
}
let lock;
try {
const response = await fetch(lockfileUrl, {
mode: 'cors',
credentials: 'omit'
});
lock = await response.json();
}
catch (reason) {
console.warn(`Failed to load the Pyodide lockfile: ${reason}`);
return;
}
let requirementsText;
try {
const python = lock['info']['python'];
const abi = (_d = (_c = (_b = (_a = lock['packages']['numpy']) === null || _a === void 0 ? void 0 : _a['file_name']) === null || _b === void 0 ? void 0 : _b.split('-')) === null || _c === void 0 ? void 0 : _c.pop()) === null || _d === void 0 ? void 0 : _d.slice(0, -'_wasm32.whl'.length);
const packageKeys = new Array();
const packageVersions = new Map();
for (const package__ of Object.values(lock['packages'])) {
const package_ = package__;
if (package_['package_type'] != 'package') {
continue;
}
if (!package_['file_name'].endsWith('.whl')) {
continue;
}
if (package_['install_dir'] != 'site') {
continue;
}
if (IGNORE_PACKAGES.includes(package_['name'])) {
continue;
}
const key = package_['name'].toLowerCase();
packageKeys.push(key);
packageVersions.set(key, {
name: package_['name'],
version: package_['version']
});
}
const requirements = new Array();
requirements.push('# ========== Online Laboratory for Climate Science and Meteorology =========== #');
requirements.push('# #');
requirements.push('# requirements.txt #');
requirements.push('# for a custom Pyodide lockfile #');
requirements.push('# #');
requirements.push('# This list contains the locked versions of all pre-installed packages. #');
requirements.push('# ============================================================================ #');
requirements.push('');
requirements.push(`# python == ${python}`);
if (abi !== undefined) {
requirements.push(`# abi == ${abi}`);
}
requirements.push('');
for (const key of packageKeys.sort()) {
const { name, version } = packageVersions.get(key);
requirements.push(`${name} == ${version}`);
}
requirements.push('');
requirementsText = requirements.join('\n');
}
catch (reason) {
console.warn(`Failed to compute the ${REQUIREMENTS} file from the Pyodide lockfile: ${reason}`);
return;
}
try {
const requirementsPath = PathExt.join(sessionPath, REQUIREMENTS);
await drive.save(requirementsPath, {
name: REQUIREMENTS,
path: requirementsPath,
last_modified: now.toISOString(),
created: now.toISOString(),
format: 'text',
mimetype: 'text/plain',
content: requirementsText,
size: new Blob([requirementsText]).size,
writable: false,
type: 'file'
});
}
catch (reason) {
console.warn(`Failed to save the ${REQUIREMENTS} file for the new session: ${reason}`);
}
}
// do not await the creation of the requirements file
createRequirementsFileFromPyodideLockfile();
}
return { sessionPath };
}
};
export default plugin;
export * from './tokens';