Skip to content

Commit

Permalink
🔌 extend the server api for enhanced binder connections (#681)
Browse files Browse the repository at this point in the history
* ⚙️ server can now accept custom provider specs for binder connection url building
* 🔌 repoproviderspecs as default
  • Loading branch information
stevejpurves authored Sep 4, 2023
1 parent 1b2363d commit 7e1677f
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 118 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"demo": "turbo run demo",
"docs": "turbo run docs",
"test": "turbo run test",
"test:core:watch": "turbo run test:watch --filter=thebe-core",
"test:e2e": "turbo run test:e2e",
"lint": "turbo run lint",
"lint:format": "turbo run lint:format",
Expand Down Expand Up @@ -52,7 +53,7 @@
},
"homepage": "https://thebe.readthedocs.io/en/latest",
"devDependencies": {
"happy-dom": "^10.10.4",
"happy-dom": "^10.11.2",
"turbo": "^1.4.2",
"vitest": "^0.34.2"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export class Config {
private _serverSettings: Required<Omit<ServerSettings, 'wsUrl'>> & { wsUrl?: string };
private _events: ThebeEvents;

constructor(opts: CoreOptions = {}, events?: ThebeEvents) {
this._events = events ?? new ThebeEvents();
constructor(opts: CoreOptions = {}, extraConfig?: { events?: ThebeEvents }) {
this._events = extraConfig?.events ?? new ThebeEvents();

this._options = {
mathjaxUrl:
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ import type {
SavedSessionOptions,
MathjaxOptions,
} from './types';
import { RepoProvider } from './types';

export function makeBinderOptions(opts: BinderOptions) {
return {
repo: 'executablebooks/thebe-binder-base',
ref: 'HEAD',
binderUrl: 'https://mybinder.org',
repoProvider: RepoProvider.github,
repoProvider: 'github',
...opts,
};
}
Expand Down Expand Up @@ -58,14 +57,14 @@ export function makeConfiguration(
options: CoreOptions & { [k: string]: any },
events?: ThebeEvents,
) {
return new Config(options, events);
return new Config(options, { events });
}

export function ensureCoreOptions(
options: CoreOptions & { [k: string]: any },
events?: ThebeEvents,
): Required<CoreOptions> {
const config = new Config(options, events);
const config = new Config(options, { events });

return {
...config.base,
Expand Down
32 changes: 10 additions & 22 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
KernelOptions,
RepoProviderSpec,
RestAPIContentsResponse,
ServerRestAPI,
ServerRuntime,
Expand All @@ -11,8 +12,8 @@ import type { ServiceManager } from '@jupyterlab/services';
import type { LiteServerConfig } from 'thebe-lite';
import type { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import type { StatusEvent } from './events';
import { RepoProvider } from './types';
import { makeGitHubUrl, makeGitLabUrl, makeGitUrl } from './url';
import { WellKnownRepoProvider } from './types';
import { WELL_KNOWN_REPO_PROVIDERS, makeBinderUrl } from './url';
import { getExistingServer, makeStorageKey, saveServerInfo } from './sessions';
import {
KernelManager,
Expand All @@ -37,6 +38,7 @@ class ThebeServer implements ServerRuntime, ServerRestAPI {
readonly ready: Promise<ThebeServer>;
sessionManager?: SessionManager;
serviceManager?: ServiceManager; // jlite only
repoProviders?: RepoProviderSpec[];
private resolveReadyFn?: (value: ThebeServer | PromiseLike<ThebeServer>) => void;
private _isDisposed: boolean;
private events: EventEmitter;
Expand Down Expand Up @@ -243,25 +245,8 @@ class ThebeServer implements ServerRuntime, ServerRestAPI {
});
}

_makeBinderUrl() {
let url: string;
switch (this.config.binder.repoProvider) {
case RepoProvider.git:
url = makeGitUrl(this.config.binder);
break;
case RepoProvider.gitlab:
url = makeGitLabUrl(this.config.binder);
break;
case RepoProvider.github:
default:
url = makeGitHubUrl(this.config.binder);
break;
}
return url;
}

async checkForSavedBinderSession() {
const url = this._makeBinderUrl();
const url = makeBinderUrl(this.config.binder, this.repoProviders ?? WELL_KNOWN_REPO_PROVIDERS);
return getExistingServer(this.config.savedSessions, url);
}

Expand All @@ -273,14 +258,17 @@ class ThebeServer implements ServerRuntime, ServerRestAPI {
* @param opts
* @returns
*/
async connectToServerViaBinder(): Promise<void> {
async connectToServerViaBinder(customProviders?: RepoProviderSpec[]): Promise<void> {
// request new server
this.events.triggerStatus({
status: ServerStatusEvent.launching,
message: `Connecting to binderhub at ${this.config.binder.binderUrl}`,
});

const url = this._makeBinderUrl();
// TODO binder connection setup probably better as a a factory independent of the server
this.repoProviders = [...WELL_KNOWN_REPO_PROVIDERS, ...(customProviders ?? [])];

const url = makeBinderUrl(this.config.binder, this.repoProviders);

this.events.triggerStatus({
status: ServerStatusEvent.launching,
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export interface CoreOptions {
serverSettings?: ServerSettings;
}

export enum RepoProvider {
'git' = 'git',
'github' = 'github',
'gitlab' = 'gitlab',
'gist' = 'gist',
export interface RepoProviderSpec {
name: string;
makeUrl: (opts: BinderOptions) => string;
}

export type WellKnownRepoProvider = 'git' | 'github' | 'gitlab' | 'gist';

export type MathjaxOptions = Pick<CoreOptions, 'mathjaxConfig' | 'mathjaxUrl'>;

export interface SavedSessionOptions {
Expand All @@ -58,7 +58,7 @@ export interface BinderOptions {
repo?: string;
ref?: string;
binderUrl?: string;
repoProvider?: RepoProvider;
repoProvider?: WellKnownRepoProvider | string;
}

export interface ServerSettings {
Expand Down
79 changes: 57 additions & 22 deletions packages/core/src/url.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { BinderOptions } from './types';
import { RepoProvider } from './types';

function throwOnBadProvider(actual: RepoProvider, expected: RepoProvider) {
if (actual !== expected) throw Error(`Bad Provider, expected ${expected} got ${actual}`);
}
import type { BinderOptions, RepoProviderSpec } from './types';
import { WellKnownRepoProvider } from './types';

/**
* Make a binder url for git providers
Expand All @@ -15,11 +11,11 @@ function throwOnBadProvider(actual: RepoProvider, expected: RepoProvider) {
* @param opts BinderOptions
* @returns a binder compatible url
*/
export function makeGitUrl(opts: Required<BinderOptions>): string {
throwOnBadProvider(opts.repoProvider, RepoProvider.git);
function makeGitUrl(opts: BinderOptions): string {
if (!opts.repo) throw Error('repo is required for git provider');
const { repo, binderUrl, ref } = opts;
const encodedRepo = encodeURIComponent(repo.replace(/(^\/)|(\/?$)/g, ''));
return `${binderUrl.replace(/(\/?$)/g, '')}/build/git/${encodedRepo}/${ref}`;
return `${binderUrl?.replace(/(\/?$)/g, '')}/build/git/${encodedRepo}/${ref ?? 'HEAD'}`;
}

/**
Expand All @@ -33,13 +29,13 @@ export function makeGitUrl(opts: Required<BinderOptions>): string {
* @param opts BinderOptions
* @returns a binder compatible url
*/
export function makeGitLabUrl(opts: Required<BinderOptions>): string {
throwOnBadProvider(opts.repoProvider, RepoProvider.gitlab);
const binderUrl = opts.binderUrl.replace(/(\/?$)/g, '');
function makeGitLabUrl(opts: BinderOptions): string {
if (!opts.repo) throw Error('repo is required for gitlab provider');
const binderUrl = opts.binderUrl?.replace(/(\/?$)/g, '');
const repo = encodeURIComponent(
opts.repo.replace(/^(https?:\/\/)?gitlab.com\//, '').replace(/(^\/)|(\/?$)/g, ''),
(opts.repo ?? '').replace(/^(https?:\/\/)?gitlab.com\//, '').replace(/(^\/)|(\/?$)/g, ''),
);
return `${binderUrl}/build/gl/${repo}/${opts.ref}`;
return `${binderUrl}/build/gl/${repo}/${opts.ref ?? 'HEAD'}`;
}

/**
Expand All @@ -53,16 +49,55 @@ export function makeGitLabUrl(opts: Required<BinderOptions>): string {
* @param opts BinderOptions
* @returns a binder compatible url
*/
export function makeGitHubUrl(opts: Required<BinderOptions>): string {
throwOnBadProvider(opts.repoProvider, RepoProvider.github);
function makeGitHubUrl(opts: BinderOptions): string {
if (!opts.repo) throw Error('repo is required for github provider');
const repo = opts.repo.replace(/^(https?:\/\/)?github.com\//, '').replace(/(^\/)|(\/?$)/g, '');
const binderUrl = opts.binderUrl.replace(/(\/?$)/g, '');
return `${binderUrl}/build/gh/${repo}/${opts.ref}`;
const binderUrl = opts.binderUrl?.replace(/(\/?$)/g, '');
return `${binderUrl}/build/gh/${repo}/${opts.ref ?? 'HEAD'}`;
}

export function makeGistUrl(opts: Required<BinderOptions>): string {
throwOnBadProvider(opts.repoProvider, RepoProvider.gist);
function makeGistUrl(opts: BinderOptions): string {
if (!opts.repo) throw Error('repo is required for gist provider');
const repo = opts.repo.replace(/^(https?:\/\/)?github.com\//, '').replace(/(^\/)|(\/?$)/g, '');
const binderUrl = opts.binderUrl.replace(/(\/?$)/g, '');
return `${binderUrl}/build/gist/${repo}/${opts.ref}`;
const binderUrl = opts.binderUrl?.replace(/(\/?$)/g, '');
return `${binderUrl}/build/gist/${repo}/${opts.ref ?? 'HEAD'}`;
}

export const GITHUB_SPEC: RepoProviderSpec = {
name: 'github',
makeUrl: makeGitHubUrl,
};

export const GITLAB_SPEC: RepoProviderSpec = {
name: 'gitlab',
makeUrl: makeGitLabUrl,
};

export const GIT_SPEC: RepoProviderSpec = {
name: 'git',
makeUrl: makeGitUrl,
};

export const GIST_SPEC: RepoProviderSpec = {
name: 'gist',
makeUrl: makeGistUrl,
};

export const WELL_KNOWN_REPO_PROVIDERS = [GITHUB_SPEC, GITLAB_SPEC, GIT_SPEC, GIST_SPEC];

/**
* Make a binder url for both well known or custom providers
*
* Custom providers are supported by passing in an array of CustomRepoProviderSpecs.
*
*/
export function makeBinderUrl(opts: BinderOptions, repoProviders: RepoProviderSpec[]): string {
const providerMap: Record<string, RepoProviderSpec> =
repoProviders.reduce((obj, spec) => ({ ...obj, [spec.name]: spec }), {}) ?? {};

const provider = opts.repoProvider ?? 'github';
if (!Object.keys(providerMap).includes(provider))
throw Error(`Unknown provider ${opts.repoProvider}`);

return providerMap[provider].makeUrl(opts);
}
2 changes: 1 addition & 1 deletion packages/core/tests/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('config', () => {
repo: 'executablebooks/thebe-binder-base',
ref: 'HEAD',
binderUrl: 'https://mybinder.org',
repoProvider: RepoProvider.github,
repoProvider: 'github',
});
});
test('kernels', () => {
Expand Down
48 changes: 0 additions & 48 deletions packages/core/tests/connect.binder.spec.ts

This file was deleted.

7 changes: 3 additions & 4 deletions packages/core/tests/options.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, test, expect } from 'vitest';

import { RepoProvider } from '../src/types';
import {
makeBinderOptions,
makeKernelOptions,
Expand All @@ -15,7 +14,7 @@ describe('options', () => {
repo: 'executablebooks/thebe-binder-base',
ref: 'HEAD',
binderUrl: 'https://mybinder.org',
repoProvider: RepoProvider.github,
repoProvider: 'github',
});
});
test('override all', () => {
Expand All @@ -24,13 +23,13 @@ describe('options', () => {
repo: 'abc',
ref: 'x',
binderUrl: 'anystring',
repoProvider: RepoProvider.git,
repoProvider: 'git',
}),
).toEqual({
repo: 'abc',
ref: 'x',
binderUrl: 'anystring',
repoProvider: RepoProvider.git,
repoProvider: 'git',
});
});
});
Expand Down
Loading

0 comments on commit 7e1677f

Please sign in to comment.