@fails-components/jupyter-filesystem-extension 0.0.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Marten Richter
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # fails_components_jupyter_filesystem_extension
2
+
3
+ [![Github Actions Status](https://github.com/fails-components/jupyterfails/workflows/Build/badge.svg)](https://github.com/fails-components/jupyterfails/actions/workflows/build.yml)
4
+
5
+ This is an extension, that taps into jupyter lite's filesystem handling to be able to remotely control and supply the files visible to jupyter lite.
6
+ It is probably only useful together with fails-components' jupyter launcher plugin,
7
+ that allows to control jupyter lite embedded inside an iframe (build from the jupyterfails' repos configs).
8
+
9
+ ## Requirements
10
+
11
+ - JupyterLab >= 4.0.0
12
+
13
+ ## Install
14
+
15
+ To install the extension, execute:
16
+
17
+ ```bash
18
+ pip install fails_components_jupyter_filesystem_extension
19
+ ```
20
+
21
+ ## Uninstall
22
+
23
+ To remove the extension, execute:
24
+
25
+ ```bash
26
+ pip uninstall fails_components_jupyter_filesystem_extension
27
+ ```
28
+
29
+ ## Contributing
30
+
31
+ ### Development install
32
+
33
+ Note: You will need NodeJS to build the extension package.
34
+
35
+ The `jlpm` command is JupyterLab's pinned version of
36
+ [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
37
+ `yarn` or `npm` in lieu of `jlpm` below.
38
+
39
+ ```bash
40
+ # Clone the repo to your local environment
41
+ # Change directory to the fails_components_jupyter_filesystem_extension directory
42
+ jlpm build
43
+ # Install package in development mode
44
+ pip install -e "."
45
+ # Link your development version of the extension with JupyterLab
46
+ jupyter labextension develop . --overwrite
47
+ # Rebuild extension Typescript source after making changes
48
+ jlpm build
49
+ ```
50
+
51
+ You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
52
+
53
+ ```bash
54
+ # Watch the source directory in one terminal, automatically rebuilding when needed
55
+ jlpm watch
56
+ # Run JupyterLab in another terminal
57
+ jupyter lab
58
+ ```
59
+
60
+ With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
61
+
62
+ By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
63
+
64
+ ```bash
65
+ jupyter lab build --minimize=False
66
+ ```
67
+
68
+ ### Development uninstall
69
+
70
+ ```bash
71
+ pip uninstall fails_components_jupyter_filesystem_extension
72
+ ```
73
+
74
+ In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
75
+ command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
76
+ folder is located. Then you can remove the symlink named `@fails-components/jupyter-applet-view` within that folder.
77
+
78
+ ### Testing the extension
79
+
80
+ #### Frontend tests
81
+
82
+ This extension is using [Jest](https://jestjs.io/) for JavaScript code testing.
83
+
84
+ To execute them, execute:
85
+
86
+ ```sh
87
+ jlpm
88
+ jlpm test
89
+ ```
90
+
91
+ <!--
92
+ #### Integration tests
93
+
94
+ This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests).
95
+ More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
96
+
97
+ More information are provided within the [ui-tests](./ui-tests/README.md) README.
98
+
99
+ ### Packaging the extension
100
+
101
+ See [RELEASE](RELEASE.md)
102
+ -->
@@ -0,0 +1,31 @@
1
+ import { Contents as ServerContents } from '@jupyterlab/services';
2
+ import { IContents } from '@jupyterlite/contents';
3
+ import { IContentEventType } from '@fails-components/jupyter-launcher';
4
+ export declare class FailsContents implements IContents {
5
+ constructor();
6
+ onMessage(event: IContentEventType): Promise<any>;
7
+ get ready(): Promise<void>;
8
+ initialize(): Promise<void>;
9
+ get(path: string, options?: ServerContents.IFetchOptions): Promise<ServerContents.IModel | null>;
10
+ save(path: string, options?: Partial<ServerContents.IModel>): Promise<ServerContents.IModel | null>;
11
+ newUntitled(options?: ServerContents.ICreateOptions): Promise<ServerContents.IModel | null>;
12
+ rename(oldLocalPath: string, newLocalPath: string): Promise<ServerContents.IModel>;
13
+ delete(path: string): Promise<void>;
14
+ copy(path: string, toDir: string): Promise<ServerContents.IModel>;
15
+ createCheckpoint(path: string): Promise<ServerContents.ICheckpointModel>;
16
+ listCheckpoints(path: string): Promise<ServerContents.ICheckpointModel[]>;
17
+ restoreCheckpoint(path: string, checkpointID: string): Promise<void>;
18
+ deleteCheckpoint(path: string, checkpointID: string): Promise<void>;
19
+ static EMPTY_NB: {
20
+ metadata: {
21
+ orig_nbformat: number;
22
+ };
23
+ nbformat_minor: number;
24
+ nbformat: number;
25
+ cells: never[];
26
+ };
27
+ private _ready;
28
+ private _fileContent;
29
+ private _fileName;
30
+ private _failsCallbacks;
31
+ }
@@ -0,0 +1,165 @@
1
+ import { PromiseDelegate } from '@lumino/coreutils';
2
+ // portions used from Jupyterlab:
3
+ /* -----------------------------------------------------------------------------
4
+ | Copyright (c) Jupyter Development Team.
5
+ | Distributed under the terms of the Modified BSD License.
6
+ |----------------------------------------------------------------------------*/
7
+ // This code contains portions from or is inspired by Jupyter lab and lite
8
+ const jsonMime = 'application/json';
9
+ class FailsContents {
10
+ constructor() {
11
+ this._fileContent = JSON.stringify(FailsContents.EMPTY_NB);
12
+ this._fileName = 'unloaded.ipynb';
13
+ this._ready = new PromiseDelegate();
14
+ if (!window.failsCallbacks) {
15
+ window.failsCallbacks = {};
16
+ }
17
+ this._failsCallbacks = window.failsCallbacks;
18
+ this._failsCallbacks.callContents = this.onMessage.bind(this);
19
+ }
20
+ async onMessage(event) {
21
+ // todo handle events
22
+ switch (event.task) {
23
+ case 'loadFile':
24
+ {
25
+ const loadevent = event;
26
+ this._fileContent = JSON.stringify(loadevent.fileData || FailsContents.EMPTY_NB);
27
+ this._fileName = loadevent.fileName;
28
+ }
29
+ break;
30
+ case 'savedFile':
31
+ {
32
+ const savedevent = event;
33
+ if (this._fileName !== savedevent.fileName) {
34
+ return { error: 'Filename not found' };
35
+ }
36
+ return {
37
+ fileData: JSON.parse(this._fileContent)
38
+ };
39
+ }
40
+ break;
41
+ }
42
+ }
43
+ get ready() {
44
+ return this._ready.promise;
45
+ }
46
+ async initialize() {
47
+ this._ready.resolve(void 0);
48
+ }
49
+ async get(path, options) {
50
+ // remove leading slash
51
+ path = decodeURIComponent(path.replace(/^\//, ''));
52
+ const serverFile = {
53
+ name: this._fileName,
54
+ path: this._fileName,
55
+ last_modified: new Date(0).toISOString(),
56
+ created: new Date(0).toISOString(),
57
+ format: 'json',
58
+ mimetype: jsonMime,
59
+ content: JSON.parse(this._fileContent),
60
+ size: 0,
61
+ writable: true,
62
+ type: 'notebook'
63
+ };
64
+ if (path === '') {
65
+ // the local directory, return the info about the proxy notebook
66
+ return {
67
+ name: '',
68
+ path,
69
+ last_modified: new Date(0).toISOString(),
70
+ created: new Date(0).toISOString(),
71
+ format: 'json',
72
+ mimetype: jsonMime,
73
+ content: [serverFile],
74
+ size: 0,
75
+ writable: true,
76
+ type: 'directory'
77
+ };
78
+ }
79
+ if (path === this._fileName) {
80
+ return serverFile;
81
+ }
82
+ return null; // not found
83
+ }
84
+ async save(path, options = {}) {
85
+ path = decodeURIComponent(path);
86
+ if (path !== this._fileName) {
87
+ // we only allow the proxy object
88
+ return null;
89
+ }
90
+ const chunk = options.chunk;
91
+ const chunked = chunk ? chunk > 1 || chunk === -1 : false;
92
+ let item = await this.get(path, {
93
+ content: chunked
94
+ });
95
+ if (!item) {
96
+ return null;
97
+ }
98
+ const modified = new Date().toISOString();
99
+ // override with the new values
100
+ item = {
101
+ ...item,
102
+ ...options,
103
+ last_modified: modified
104
+ };
105
+ if (options.content && options.format === 'base64') {
106
+ const lastChunk = chunk ? chunk === -1 : true;
107
+ const modified = new Date().toISOString();
108
+ // override with the new values
109
+ item = {
110
+ ...item,
111
+ ...options,
112
+ last_modified: modified
113
+ };
114
+ const originalContent = item.content;
115
+ const escaped = decodeURIComponent(escape(atob(options.content)));
116
+ const newcontent = chunked ? originalContent + escaped : escaped;
117
+ item = {
118
+ ...item,
119
+ content: lastChunk ? JSON.parse(newcontent) : newcontent,
120
+ format: 'json',
121
+ type: 'notebook',
122
+ size: newcontent.length
123
+ };
124
+ this._fileContent = JSON.stringify(newcontent); // no parsing
125
+ return item;
126
+ }
127
+ this._fileContent = JSON.stringify(item.content); // no parsing
128
+ return item;
129
+ }
130
+ // For fails creating a new file is not allowed, so no need to implment it
131
+ async newUntitled(options) {
132
+ throw new Error('NewUntitled not implemented');
133
+ }
134
+ async rename(oldLocalPath, newLocalPath) {
135
+ throw new Error('rename not implemented');
136
+ }
137
+ async delete(path) {
138
+ throw new Error('delete not implemented');
139
+ }
140
+ async copy(path, toDir) {
141
+ throw new Error('copy not implemented');
142
+ }
143
+ async createCheckpoint(path) {
144
+ throw new Error('createCheckpoint not (yet?) implemented');
145
+ }
146
+ async listCheckpoints(path) {
147
+ // throw new Error('listCheckpoints not (yet?) implemented');
148
+ return [{ id: 'fakeCheckpoint', last_modified: new Date().toISOString() }];
149
+ }
150
+ async restoreCheckpoint(path, checkpointID) {
151
+ throw new Error('restoreCheckpoint not (yet?) implemented');
152
+ }
153
+ async deleteCheckpoint(path, checkpointID) {
154
+ throw new Error('deleteCheckpoint not (yet?) implemented');
155
+ }
156
+ }
157
+ FailsContents.EMPTY_NB = {
158
+ metadata: {
159
+ orig_nbformat: 4
160
+ },
161
+ nbformat_minor: 5,
162
+ nbformat: 4,
163
+ cells: []
164
+ };
165
+ export { FailsContents };
package/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { IContents } from '@jupyterlite/contents';
2
+ import { JupyterLiteServerPlugin } from '@jupyterlite/server';
3
+ export declare const failsContentsPlugin: JupyterLiteServerPlugin<IContents>;
4
+ declare const plugins: JupyterLiteServerPlugin<any>[];
5
+ export default plugins;
package/lib/index.js ADDED
@@ -0,0 +1,37 @@
1
+ import { IContents } from '@jupyterlite/contents';
2
+ import { ISettings } from '@jupyterlite/settings';
3
+ import { FailsContents } from './contents';
4
+ import { FailsSettings } from './settings';
5
+ export const failsContentsPlugin = {
6
+ id: '@fails-components/jupyter-fails-server:contents',
7
+ requires: [],
8
+ autoStart: true,
9
+ provides: IContents,
10
+ activate: (app) => {
11
+ if (app.namespace !== 'JupyterLite Server') {
12
+ console.log('Not on server');
13
+ }
14
+ const contents = new FailsContents();
15
+ app.started.then(() => contents.initialize().catch(console.warn));
16
+ return contents;
17
+ }
18
+ };
19
+ const failsSettingsPlugin = {
20
+ id: '@fails-components/jupyter-fails-server:settings',
21
+ requires: [],
22
+ autoStart: true,
23
+ provides: ISettings,
24
+ activate: (app) => {
25
+ if (app.namespace !== 'JupyterLite Server') {
26
+ console.log('Not on server');
27
+ }
28
+ const settings = new FailsSettings();
29
+ app.started.then(() => settings.initialize().catch(console.warn));
30
+ return settings;
31
+ }
32
+ };
33
+ const plugins = [
34
+ failsContentsPlugin,
35
+ failsSettingsPlugin
36
+ ];
37
+ export default plugins;
@@ -0,0 +1,15 @@
1
+ import { ISettings, IPlugin as ISettingsPlugin } from '@jupyterlite/settings';
2
+ export declare class FailsSettings implements ISettings {
3
+ static _overrides: Record<string, ISettingsPlugin['schema']['default']>;
4
+ static override(plugin: ISettingsPlugin): ISettingsPlugin;
5
+ constructor();
6
+ get ready(): Promise<void>;
7
+ initialize(): Promise<void>;
8
+ get(pluginId: string): Promise<ISettingsPlugin | undefined>;
9
+ getAll(): Promise<{
10
+ settings: ISettingsPlugin[];
11
+ }>;
12
+ private _getAll;
13
+ save(pluginId: string, raw: string): Promise<void>;
14
+ private _ready;
15
+ }
@@ -0,0 +1,79 @@
1
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
2
+ import { PromiseDelegate } from '@lumino/coreutils';
3
+ import * as json5 from 'json5';
4
+ // portions used from Jupyterlab:
5
+ /* -----------------------------------------------------------------------------
6
+ | Copyright (c) Jupyter Development Team.
7
+ | Distributed under the terms of the Modified BSD License.
8
+ |----------------------------------------------------------------------------*/
9
+ // This code contains portions from or is inspired by Jupyter lab and lite
10
+ class FailsSettings {
11
+ static override(plugin) {
12
+ if (FailsSettings._overrides[plugin.id]) {
13
+ if (!plugin.schema.properties) {
14
+ // probably malformed, or only provides keyboard shortcuts, etc.
15
+ plugin.schema.properties = {};
16
+ }
17
+ for (const [prop, propDefault] of Object.entries(FailsSettings._overrides[plugin.id] || {})) {
18
+ plugin.schema.properties[prop].default = propDefault;
19
+ }
20
+ }
21
+ return plugin;
22
+ }
23
+ constructor() {
24
+ this._ready = new PromiseDelegate();
25
+ }
26
+ get ready() {
27
+ return this._ready.promise;
28
+ }
29
+ async initialize() {
30
+ this._ready.resolve(void 0);
31
+ }
32
+ // copied from the original settings
33
+ async get(pluginId) {
34
+ const all = await this.getAll();
35
+ const settings = all.settings;
36
+ const setting = settings.find((setting) => {
37
+ return setting.id === pluginId;
38
+ });
39
+ return setting;
40
+ }
41
+ // copied from the original settings
42
+ async getAll() {
43
+ const allCore = await this._getAll('all.json');
44
+ let allFederated = [];
45
+ try {
46
+ allFederated = await this._getAll('all_federated.json');
47
+ }
48
+ catch (_a) {
49
+ // handle the case where there is no federated extension
50
+ }
51
+ // JupyterLab 4 expects all settings to be returned in one go
52
+ // so append the settings from federated plugins to the core ones
53
+ const all = allCore.concat(allFederated);
54
+ // return existing user settings if they exist
55
+ const settings = await Promise.all(all.map(async (plugin) => {
56
+ // const { id } = plugin;
57
+ const raw = /*((await storage.getItem(id)) as string) ?? */ plugin.raw;
58
+ return {
59
+ ...FailsSettings.override(plugin),
60
+ raw,
61
+ settings: json5.parse(raw)
62
+ };
63
+ }));
64
+ return { settings };
65
+ }
66
+ // one to one copy from settings of the original JupyterLite
67
+ async _getAll(file) {
68
+ var _a;
69
+ const settingsUrl = (_a = PageConfig.getOption('settingsUrl')) !== null && _a !== void 0 ? _a : '/';
70
+ const all = (await (await fetch(URLExt.join(settingsUrl, file))).json());
71
+ return all;
72
+ }
73
+ async save(pluginId, raw) {
74
+ // we do nothing
75
+ }
76
+ }
77
+ // the following is copied from the original Jupyter Lite Settings Object
78
+ FailsSettings._overrides = JSON.parse(PageConfig.getOption('settingsOverrides') || '{}');
79
+ export { FailsSettings };
package/package.json ADDED
@@ -0,0 +1,211 @@
1
+ {
2
+ "name": "@fails-components/jupyter-filesystem-extension",
3
+ "version": "0.0.1-alpha.10",
4
+ "description": "A collection of extensions, that redirect's filesystems access to fails and let fails puppeteer Jupyter lite. ",
5
+ "keywords": [
6
+ "jupyter",
7
+ "jupyterlite",
8
+ "jupyterlite-extension"
9
+ ],
10
+ "homepage": "https://github.com/fails-components/jupyterfails",
11
+ "bugs": {
12
+ "url": "https://github.com/fails-components/jupyterfails/issues"
13
+ },
14
+ "license": "BSD-3-Clause",
15
+ "author": {
16
+ "name": "Marten Richter",
17
+ "email": "marten.richter@freenet.de"
18
+ },
19
+ "files": [
20
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
21
+ "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
22
+ "schema/*.json",
23
+ "src/**/*.{ts,tsx}",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "main": "lib/index.js",
28
+ "types": "lib/index.d.ts",
29
+ "style": "style/index.css",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/fails-components/jupyterfails.git"
33
+ },
34
+ "scripts": {
35
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
36
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
37
+ "build:labextension": "jupyter labextension build .",
38
+ "build:labextension:dev": "jupyter labextension build --development True .",
39
+ "build:lib": "tsc --sourceMap",
40
+ "build:lib:prod": "tsc",
41
+ "clean": "jlpm clean:lib",
42
+ "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
43
+ "clean:lintcache": "rimraf .eslintcache .stylelintcache",
44
+ "clean:labextension": "rimraf fails_components_jupyter_filesystem_extension/labextension fails_components_jupyter_filesystem_extension/_version.py",
45
+ "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
46
+ "eslint": "jlpm eslint:check --fix",
47
+ "eslint:check": "eslint . --cache --ext .ts,.tsx",
48
+ "install:extension": "jlpm build",
49
+ "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
50
+ "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
51
+ "prepublishOnly": "npm run build",
52
+ "prettier": "jlpm prettier:base --write --list-different",
53
+ "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
54
+ "prettier:check": "jlpm prettier:base --check",
55
+ "stylelint": "jlpm stylelint:check --fix",
56
+ "stylelint:check": "stylelint --cache \"style/**/*.css\"",
57
+ "test": "jest --coverage --passWithNoTests",
58
+ "watch": "run-p watch:src watch:labextension",
59
+ "watch:src": "tsc -w --sourceMap",
60
+ "watch:labextension": "jupyter labextension watch ."
61
+ },
62
+ "dependencies": {
63
+ "@fails-components/jupyter-launcher": "^0.0.1-alpha.10",
64
+ "@jupyter-widgets/jupyterlab-manager": "^5.0.11",
65
+ "@jupyterlab/application": "^4.3.4",
66
+ "@jupyterlab/services": "^7.3.4",
67
+ "@jupyterlite/application-extension": "^0.5.0",
68
+ "@jupyterlite/contents": "^0.5.0",
69
+ "@jupyterlite/server": "^0.5.0",
70
+ "@jupyterlite/settings": "^0.5.0",
71
+ "@lumino/coreutils": "^2.2.0",
72
+ "json5": "^2.2.3"
73
+ },
74
+ "devDependencies": {
75
+ "@jupyterlab/builder": "^4.0.0",
76
+ "@jupyterlab/testutils": "^4.0.0",
77
+ "@types/jest": "^29.2.0",
78
+ "@types/json-schema": "^7.0.11",
79
+ "@types/react": "^18.0.26",
80
+ "@types/react-addons-linked-state-mixin": "^0.14.22",
81
+ "@typescript-eslint/eslint-plugin": "^6.1.0",
82
+ "@typescript-eslint/parser": "^6.1.0",
83
+ "css-loader": "^6.7.1",
84
+ "eslint": "^8.36.0",
85
+ "eslint-config-prettier": "^8.8.0",
86
+ "eslint-plugin-prettier": "^5.0.0",
87
+ "jest": "^29.2.0",
88
+ "npm-run-all": "^4.1.5",
89
+ "prettier": "^3.0.0",
90
+ "rimraf": "^5.0.1",
91
+ "source-map-loader": "^1.0.2",
92
+ "style-loader": "^3.3.1",
93
+ "stylelint": "^15.10.1",
94
+ "stylelint-config-recommended": "^13.0.0",
95
+ "stylelint-config-standard": "^34.0.0",
96
+ "stylelint-csstree-validator": "^3.0.0",
97
+ "stylelint-prettier": "^4.0.0",
98
+ "typescript": "~5.0.2",
99
+ "yjs": "^13.5.0"
100
+ },
101
+ "sideEffects": [
102
+ "style/*.css",
103
+ "style/index.js"
104
+ ],
105
+ "styleModule": "style/index.js",
106
+ "publishConfig": {
107
+ "access": "public"
108
+ },
109
+ "jupyterlab": {
110
+ "extension": true,
111
+ "outputDir": "fails_components_jupyter_filesystem_extension/labextension",
112
+ "schemaDir": "schema"
113
+ },
114
+ "jupyterlite": {
115
+ "liteExtension": true
116
+ },
117
+ "eslintIgnore": [
118
+ "node_modules",
119
+ "dist",
120
+ "coverage",
121
+ "**/*.d.ts",
122
+ "tests",
123
+ "**/__tests__",
124
+ "ui-tests"
125
+ ],
126
+ "eslintConfig": {
127
+ "extends": [
128
+ "eslint:recommended",
129
+ "plugin:@typescript-eslint/eslint-recommended",
130
+ "plugin:@typescript-eslint/recommended",
131
+ "plugin:prettier/recommended"
132
+ ],
133
+ "parser": "@typescript-eslint/parser",
134
+ "parserOptions": {
135
+ "project": "tsconfig.json",
136
+ "sourceType": "module"
137
+ },
138
+ "plugins": [
139
+ "@typescript-eslint"
140
+ ],
141
+ "rules": {
142
+ "@typescript-eslint/naming-convention": [
143
+ "error",
144
+ {
145
+ "selector": "interface",
146
+ "format": [
147
+ "PascalCase"
148
+ ],
149
+ "custom": {
150
+ "regex": "^I[A-Z]",
151
+ "match": true
152
+ }
153
+ }
154
+ ],
155
+ "@typescript-eslint/no-unused-vars": [
156
+ "warn",
157
+ {
158
+ "args": "none"
159
+ }
160
+ ],
161
+ "@typescript-eslint/no-explicit-any": "off",
162
+ "@typescript-eslint/no-namespace": "off",
163
+ "@typescript-eslint/no-use-before-define": "off",
164
+ "@typescript-eslint/quotes": [
165
+ "error",
166
+ "single",
167
+ {
168
+ "avoidEscape": true,
169
+ "allowTemplateLiterals": false
170
+ }
171
+ ],
172
+ "curly": [
173
+ "error",
174
+ "all"
175
+ ],
176
+ "eqeqeq": "error",
177
+ "prefer-arrow-callback": "error"
178
+ }
179
+ },
180
+ "prettier": {
181
+ "singleQuote": true,
182
+ "trailingComma": "none",
183
+ "arrowParens": "avoid",
184
+ "endOfLine": "auto",
185
+ "overrides": [
186
+ {
187
+ "files": "package.json",
188
+ "options": {
189
+ "tabWidth": 4
190
+ }
191
+ }
192
+ ]
193
+ },
194
+ "stylelint": {
195
+ "extends": [
196
+ "stylelint-config-recommended",
197
+ "stylelint-config-standard",
198
+ "stylelint-prettier/recommended"
199
+ ],
200
+ "plugins": [
201
+ "stylelint-csstree-validator"
202
+ ],
203
+ "rules": {
204
+ "csstree/validator": true,
205
+ "property-no-vendor-prefix": null,
206
+ "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
207
+ "selector-no-vendor-prefix": null,
208
+ "value-no-vendor-prefix": null
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,218 @@
1
+ import { Contents as ServerContents } from '@jupyterlab/services';
2
+ import { IContents } from '@jupyterlite/contents';
3
+ import { PromiseDelegate } from '@lumino/coreutils';
4
+ import {
5
+ IFailsCallbacks,
6
+ IContentEventType,
7
+ ILoadJupyterContentEvent,
8
+ ISavedJupyterContentEvent
9
+ } from '@fails-components/jupyter-launcher';
10
+
11
+ // portions used from Jupyterlab:
12
+ /* -----------------------------------------------------------------------------
13
+ | Copyright (c) Jupyter Development Team.
14
+ | Distributed under the terms of the Modified BSD License.
15
+ |----------------------------------------------------------------------------*/
16
+ // This code contains portions from or is inspired by Jupyter lab and lite
17
+
18
+ const jsonMime = 'application/json';
19
+
20
+ export class FailsContents implements IContents {
21
+ constructor() {
22
+ this._ready = new PromiseDelegate();
23
+ if (!(window as any).failsCallbacks) {
24
+ (window as any).failsCallbacks = {};
25
+ }
26
+ this._failsCallbacks = (window as any).failsCallbacks;
27
+ this._failsCallbacks.callContents = this.onMessage.bind(this);
28
+ }
29
+
30
+ async onMessage(event: IContentEventType): Promise<any> {
31
+ // todo handle events
32
+ switch (event.task) {
33
+ case 'loadFile':
34
+ {
35
+ const loadevent = event as ILoadJupyterContentEvent;
36
+ this._fileContent = JSON.stringify(
37
+ loadevent.fileData || FailsContents.EMPTY_NB
38
+ );
39
+ this._fileName = loadevent.fileName;
40
+ }
41
+ break;
42
+ case 'savedFile':
43
+ {
44
+ const savedevent = event as ISavedJupyterContentEvent;
45
+ if (this._fileName !== savedevent.fileName) {
46
+ return { error: 'Filename not found' };
47
+ }
48
+ return {
49
+ fileData: JSON.parse(this._fileContent)
50
+ };
51
+ }
52
+ break;
53
+ }
54
+ }
55
+
56
+ get ready(): Promise<void> {
57
+ return this._ready.promise;
58
+ }
59
+
60
+ async initialize() {
61
+ this._ready.resolve(void 0);
62
+ }
63
+
64
+ async get(
65
+ path: string,
66
+ options?: ServerContents.IFetchOptions
67
+ ): Promise<ServerContents.IModel | null> {
68
+ // remove leading slash
69
+ path = decodeURIComponent(path.replace(/^\//, ''));
70
+
71
+ const serverFile = {
72
+ name: this._fileName,
73
+ path: this._fileName,
74
+ last_modified: new Date(0).toISOString(),
75
+ created: new Date(0).toISOString(),
76
+ format: 'json' as ServerContents.FileFormat,
77
+ mimetype: jsonMime,
78
+ content: JSON.parse(this._fileContent),
79
+ size: 0,
80
+ writable: true,
81
+ type: 'notebook'
82
+ };
83
+
84
+ if (path === '') {
85
+ // the local directory, return the info about the proxy notebook
86
+ return {
87
+ name: '',
88
+ path,
89
+ last_modified: new Date(0).toISOString(),
90
+ created: new Date(0).toISOString(),
91
+ format: 'json',
92
+ mimetype: jsonMime,
93
+ content: [serverFile],
94
+ size: 0,
95
+ writable: true,
96
+ type: 'directory'
97
+ };
98
+ }
99
+ if (path === this._fileName) {
100
+ return serverFile;
101
+ }
102
+ return null; // not found
103
+ }
104
+
105
+ async save(
106
+ path: string,
107
+ options: Partial<ServerContents.IModel> = {}
108
+ ): Promise<ServerContents.IModel | null> {
109
+ path = decodeURIComponent(path);
110
+ if (path !== this._fileName) {
111
+ // we only allow the proxy object
112
+ return null;
113
+ }
114
+ const chunk = options.chunk;
115
+ const chunked = chunk ? chunk > 1 || chunk === -1 : false;
116
+
117
+ let item: ServerContents.IModel | null = await this.get(path, {
118
+ content: chunked
119
+ });
120
+
121
+ if (!item) {
122
+ return null;
123
+ }
124
+
125
+ const modified = new Date().toISOString();
126
+ // override with the new values
127
+ item = {
128
+ ...item,
129
+ ...options,
130
+ last_modified: modified
131
+ };
132
+
133
+ if (options.content && options.format === 'base64') {
134
+ const lastChunk = chunk ? chunk === -1 : true;
135
+
136
+ const modified = new Date().toISOString();
137
+ // override with the new values
138
+ item = {
139
+ ...item,
140
+ ...options,
141
+ last_modified: modified
142
+ };
143
+
144
+ const originalContent = item.content;
145
+ const escaped = decodeURIComponent(escape(atob(options.content)));
146
+ const newcontent = chunked ? originalContent + escaped : escaped;
147
+ item = {
148
+ ...item,
149
+ content: lastChunk ? JSON.parse(newcontent) : newcontent,
150
+ format: 'json',
151
+ type: 'notebook',
152
+ size: newcontent.length
153
+ };
154
+ this._fileContent = JSON.stringify(newcontent); // no parsing
155
+ return item;
156
+ }
157
+
158
+ this._fileContent = JSON.stringify(item.content); // no parsing
159
+ return item;
160
+ }
161
+
162
+ // For fails creating a new file is not allowed, so no need to implment it
163
+ async newUntitled(
164
+ options?: ServerContents.ICreateOptions
165
+ ): Promise<ServerContents.IModel | null> {
166
+ throw new Error('NewUntitled not implemented');
167
+ }
168
+
169
+ async rename(
170
+ oldLocalPath: string,
171
+ newLocalPath: string
172
+ ): Promise<ServerContents.IModel> {
173
+ throw new Error('rename not implemented');
174
+ }
175
+
176
+ async delete(path: string): Promise<void> {
177
+ throw new Error('delete not implemented');
178
+ }
179
+
180
+ async copy(path: string, toDir: string): Promise<ServerContents.IModel> {
181
+ throw new Error('copy not implemented');
182
+ }
183
+
184
+ async createCheckpoint(
185
+ path: string
186
+ ): Promise<ServerContents.ICheckpointModel> {
187
+ throw new Error('createCheckpoint not (yet?) implemented');
188
+ }
189
+
190
+ async listCheckpoints(
191
+ path: string
192
+ ): Promise<ServerContents.ICheckpointModel[]> {
193
+ // throw new Error('listCheckpoints not (yet?) implemented');
194
+ return [{ id: 'fakeCheckpoint', last_modified: new Date().toISOString() }];
195
+ }
196
+
197
+ async restoreCheckpoint(path: string, checkpointID: string): Promise<void> {
198
+ throw new Error('restoreCheckpoint not (yet?) implemented');
199
+ }
200
+
201
+ async deleteCheckpoint(path: string, checkpointID: string): Promise<void> {
202
+ throw new Error('deleteCheckpoint not (yet?) implemented');
203
+ }
204
+
205
+ static EMPTY_NB = {
206
+ metadata: {
207
+ orig_nbformat: 4
208
+ },
209
+ nbformat_minor: 5,
210
+ nbformat: 4,
211
+ cells: []
212
+ };
213
+
214
+ private _ready: PromiseDelegate<void>;
215
+ private _fileContent: string = JSON.stringify(FailsContents.EMPTY_NB);
216
+ private _fileName: string = 'unloaded.ipynb';
217
+ private _failsCallbacks: IFailsCallbacks;
218
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { IContents } from '@jupyterlite/contents';
2
+ import {
3
+ JupyterLiteServerPlugin,
4
+ JupyterLiteServer
5
+ } from '@jupyterlite/server';
6
+ import { ISettings } from '@jupyterlite/settings';
7
+ import { FailsContents } from './contents';
8
+ import { FailsSettings } from './settings';
9
+
10
+ export const failsContentsPlugin: JupyterLiteServerPlugin<IContents> = {
11
+ id: '@fails-components/jupyter-fails-server:contents',
12
+ requires: [],
13
+ autoStart: true,
14
+ provides: IContents,
15
+ activate: (app: JupyterLiteServer) => {
16
+ if (app.namespace !== 'JupyterLite Server') {
17
+ console.log('Not on server');
18
+ }
19
+ const contents = new FailsContents();
20
+ app.started.then(() => contents.initialize().catch(console.warn));
21
+ return contents;
22
+ }
23
+ };
24
+
25
+ const failsSettingsPlugin: JupyterLiteServerPlugin<ISettings> = {
26
+ id: '@fails-components/jupyter-fails-server:settings',
27
+ requires: [],
28
+ autoStart: true,
29
+ provides: ISettings,
30
+ activate: (app: JupyterLiteServer) => {
31
+ if (app.namespace !== 'JupyterLite Server') {
32
+ console.log('Not on server');
33
+ }
34
+ const settings = new FailsSettings();
35
+ app.started.then(() => settings.initialize().catch(console.warn));
36
+ return settings;
37
+ }
38
+ };
39
+
40
+ const plugins: JupyterLiteServerPlugin<any>[] = [
41
+ failsContentsPlugin,
42
+ failsSettingsPlugin
43
+ ];
44
+
45
+ export default plugins;
@@ -0,0 +1,100 @@
1
+ import { PageConfig, URLExt } from '@jupyterlab/coreutils';
2
+ import { ISettings, IPlugin as ISettingsPlugin } from '@jupyterlite/settings';
3
+ import { PromiseDelegate } from '@lumino/coreutils';
4
+ import * as json5 from 'json5';
5
+
6
+ // portions used from Jupyterlab:
7
+ /* -----------------------------------------------------------------------------
8
+ | Copyright (c) Jupyter Development Team.
9
+ | Distributed under the terms of the Modified BSD License.
10
+ |----------------------------------------------------------------------------*/
11
+ // This code contains portions from or is inspired by Jupyter lab and lite
12
+
13
+ export class FailsSettings implements ISettings {
14
+ // the following is copied from the original Jupyter Lite Settings Object
15
+ static _overrides: Record<string, ISettingsPlugin['schema']['default']> =
16
+ JSON.parse(PageConfig.getOption('settingsOverrides') || '{}');
17
+
18
+ static override(plugin: ISettingsPlugin): ISettingsPlugin {
19
+ if (FailsSettings._overrides[plugin.id]) {
20
+ if (!plugin.schema.properties) {
21
+ // probably malformed, or only provides keyboard shortcuts, etc.
22
+ plugin.schema.properties = {};
23
+ }
24
+ for (const [prop, propDefault] of Object.entries(
25
+ FailsSettings._overrides[plugin.id] || {}
26
+ )) {
27
+ plugin.schema.properties[prop].default = propDefault;
28
+ }
29
+ }
30
+ return plugin;
31
+ }
32
+
33
+ constructor() {
34
+ this._ready = new PromiseDelegate();
35
+ }
36
+
37
+ get ready(): Promise<void> {
38
+ return this._ready.promise;
39
+ }
40
+
41
+ async initialize() {
42
+ this._ready.resolve(void 0);
43
+ }
44
+
45
+ // copied from the original settings
46
+ async get(pluginId: string): Promise<ISettingsPlugin | undefined> {
47
+ const all = await this.getAll();
48
+ const settings = all.settings as ISettingsPlugin[];
49
+ const setting = settings.find((setting: ISettingsPlugin) => {
50
+ return setting.id === pluginId;
51
+ });
52
+ return setting;
53
+ }
54
+
55
+ // copied from the original settings
56
+ async getAll(): Promise<{ settings: ISettingsPlugin[] }> {
57
+ const allCore = await this._getAll('all.json');
58
+ let allFederated: ISettingsPlugin[] = [];
59
+ try {
60
+ allFederated = await this._getAll('all_federated.json');
61
+ } catch {
62
+ // handle the case where there is no federated extension
63
+ }
64
+
65
+ // JupyterLab 4 expects all settings to be returned in one go
66
+ // so append the settings from federated plugins to the core ones
67
+ const all = allCore.concat(allFederated);
68
+
69
+ // return existing user settings if they exist
70
+ const settings = await Promise.all(
71
+ all.map(async plugin => {
72
+ // const { id } = plugin;
73
+ const raw = /*((await storage.getItem(id)) as string) ?? */ plugin.raw;
74
+ return {
75
+ ...FailsSettings.override(plugin),
76
+ raw,
77
+ settings: json5.parse(raw)
78
+ };
79
+ })
80
+ );
81
+ return { settings };
82
+ }
83
+
84
+ // one to one copy from settings of the original JupyterLite
85
+ private async _getAll(
86
+ file: 'all.json' | 'all_federated.json'
87
+ ): Promise<ISettingsPlugin[]> {
88
+ const settingsUrl = PageConfig.getOption('settingsUrl') ?? '/';
89
+ const all = (await (
90
+ await fetch(URLExt.join(settingsUrl, file))
91
+ ).json()) as ISettingsPlugin[];
92
+ return all;
93
+ }
94
+
95
+ async save(pluginId: string, raw: string): Promise<void> {
96
+ // we do nothing
97
+ }
98
+
99
+ private _ready: PromiseDelegate<void>;
100
+ }
package/style/base.css ADDED
@@ -0,0 +1,5 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+
4
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
5
+ */
@@ -0,0 +1 @@
1
+ @import url('base.css');
package/style/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import './base.css';
2
+ import './index.css';