Skip to content
This repository was archived by the owner on Feb 21, 2026. It is now read-only.

Commit e5b4d14

Browse files
authored
Merge pull request #11 from nteract/run-minio-on-circleci
set up minio on circle
2 parents 6f4a968 + 9ecb8bd commit e5b4d14

File tree

13 files changed

+394
-17
lines changed

13 files changed

+394
-17
lines changed

.circleci/config.yml

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ version: 2
66
jobs:
77
build:
88
docker:
9-
# specify the version you desire here
10-
# use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
11-
- image: circleci/python:3.6.1
9+
- image: circleci/python:3.6.7-node-browsers
10+
ports: 9988:9988
1211

13-
# Specify service dependencies here if necessary
14-
# CircleCI maintains a library of pre-built images
15-
# documented at https://circleci.com/docs/2.0/circleci-images/
16-
# - image: circleci/postgres:9.4
12+
- image: minio/minio:RELEASE.2018-11-06T01-01-02Z
13+
command: server /data
14+
environment:
15+
MINIO_ACCESS_KEY: ONLY_ON_CIRCLE
16+
MINIO_SECRET_KEY: CAN_WE_DO_THIS
17+
ports: 9000:9000
1718

1819
working_directory: ~/repo
1920

@@ -23,19 +24,41 @@ jobs:
2324
# Download and cache dependencies
2425
- restore_cache:
2526
keys:
26-
- v1-dependencies-{{ checksum "requirements.txt" }}
27+
- v2-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt"}}
2728
# fallback to using the latest cache if no exact match is found
28-
- v1-dependencies-
29+
- v2-dependencies-
2930

3031
- run:
3132
name: install dependencies
3233
command: |
3334
python3 -m venv venv
3435
. venv/bin/activate
36+
pip install --upgrade pip setuptools wheel
3537
pip install -r requirements.txt
3638
pip install -r requirements-dev.txt
3739
3840
- save_cache:
3941
paths:
4042
- ./venv
41-
key: v1-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
43+
key: v2-dependencies-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
44+
45+
- run:
46+
name: package up bookstore
47+
command: |
48+
. venv/bin/activate
49+
# Package up the package
50+
python setup.py sdist bdist_wheel
51+
- run:
52+
name: create virtual environment for packaged release
53+
command: |
54+
python3 -m venv venv_packaged_integration
55+
. venv_packaged_integration/bin/activate
56+
pip install --upgrade pip setuptools wheel
57+
pip install -U --force-reinstall dist/bookstore*.whl
58+
- run:
59+
name: integration tests
60+
command: |
61+
. venv_packaged_integration/bin/activate
62+
# Install the dependencies for our integration tester
63+
npm i
64+
node ci/integration.js

bookstore/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
__version__ = get_versions()['version']
44
del get_versions
55

6-
from .jupyter_server_extension import load_jupyter_server_extension, _jupyter_server_extension_paths
6+
from .handlers import load_jupyter_server_extension
7+
8+
def _jupyter_server_extension_paths():
9+
return [dict(module="bookstore")]
710

811
from .archive import BookstoreContentsArchiver
912

bookstore/archive.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def __init__(self, *args, **kwargs):
3131
# opt ourselves into being part of the Jupyter App that should have Bookstore Settings applied
3232
self.settings = BookstoreSettings(parent=self)
3333

34+
self.log.info("Archiving notebooks to {}".format(self.full_prefix))
35+
3436
self.fs = s3fs.S3FileSystem(key=self.settings.s3_access_key_id,
3537
secret=self.settings.s3_secret_access_key,
3638
client_kwargs={

bookstore/jupyter_server_extension/handlers.py renamed to bookstore/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
AuthenticatedFileHandler
55
from tornado import web
66

7-
from .._version import get_versions
7+
from ._version import get_versions
88
version = get_versions()['version']
99

1010
class BookstoreVersionHandler(APIHandler):

bookstore/jupyter_server_extension/__init__.py

Lines changed: 0 additions & 4 deletions
This file was deleted.

ci/integration.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const path = require("path");
2+
const fs = require("fs");
3+
4+
const _ = require("lodash");
5+
6+
const s3 = require("./s3");
7+
const { JupyterServer } = require("./jupyter");
8+
9+
const { sleep } = require("./sleep");
10+
11+
// Catch all rogue promise rejections to fail CI
12+
process.on("unhandledRejection", error => {
13+
console.log("unhandledRejection", error.message);
14+
console.error(error.stack);
15+
process.exit(2);
16+
});
17+
18+
console.log("running bookstore integration tests");
19+
20+
const main = async () => {
21+
const bucketName = "bookstore";
22+
23+
const jupyterServer = new JupyterServer();
24+
await jupyterServer.start();
25+
26+
const s3Config = {
27+
endPoint: "127.0.0.1",
28+
port: 9000,
29+
useSSL: false,
30+
accessKey: "ONLY_ON_CIRCLE",
31+
secretKey: "CAN_WE_DO_THIS"
32+
};
33+
34+
// Instantiate the minio client with the endpoint
35+
// and access keys as shown below.
36+
var s3Client = new s3.Client(s3Config);
37+
38+
await s3Client.makeBucket(bucketName);
39+
40+
console.log(`Created bucket ${bucketName}`);
41+
42+
const originalNotebook = {
43+
cells: [
44+
{
45+
cell_type: "code",
46+
execution_count: null,
47+
metadata: {},
48+
outputs: [],
49+
source: ["import this"]
50+
}
51+
],
52+
metadata: {
53+
kernelspec: {
54+
display_name: "Python 3",
55+
language: "python",
56+
name: "python3"
57+
},
58+
language_info: {
59+
codemirror_mode: {
60+
name: "ipython",
61+
version: 3
62+
},
63+
file_extension: ".py",
64+
mimetype: "text/x-python",
65+
name: "python",
66+
nbconvert_exporter: "python",
67+
pygments_lexer: "ipython3",
68+
version: "3.7.0"
69+
}
70+
},
71+
nbformat: 4,
72+
nbformat_minor: 2
73+
};
74+
75+
jupyterServer.writeNotebook("ci-local-writeout.ipynb", originalNotebook);
76+
77+
// Wait for minio to have the notebook
78+
// Future iterations of this script should poll to get the notebook
79+
await sleep(1000);
80+
81+
jupyterServer.shutdown();
82+
83+
/***** Check notebook from S3 *****/
84+
const rawNotebook = await s3Client.getObject(
85+
bucketName,
86+
"ci-workspace/ci-local-writeout.ipynb"
87+
);
88+
89+
const notebook = JSON.parse(rawNotebook);
90+
91+
if (!_.isEqual(notebook, originalNotebook)) {
92+
console.error("original");
93+
console.error(originalNotebook);
94+
console.error("from s3");
95+
console.error(notebook);
96+
throw new Error("Notebook on S3 does not match what we sent");
97+
}
98+
99+
console.log("Notebook on S3 matches what we sent");
100+
101+
/***** Check notebook from Disk *****/
102+
const diskNotebook = await new Promise((resolve, reject) =>
103+
fs.readFile(
104+
path.join(__dirname, "ci-local-writeout.ipynb"),
105+
(err, data) => {
106+
if (err) {
107+
reject(err);
108+
} else {
109+
resolve(JSON.parse(data));
110+
}
111+
}
112+
)
113+
);
114+
115+
if (!_.isEqual(diskNotebook, originalNotebook)) {
116+
console.error("original");
117+
console.error(originalNotebook);
118+
console.error("from disk");
119+
console.error(diskNotebook);
120+
throw new Error("Notebook on Disk does not match what we sent");
121+
}
122+
123+
console.log("📚 Bookstore Integration Complete 📚");
124+
};
125+
126+
main();

ci/jupyter.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const child_process = require("child_process");
2+
const { genToken } = require("./token");
3+
const { sleep } = require("./sleep");
4+
5+
// "Polyfill" XMLHttpRequest for rxjs' ajax to use
6+
global.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
7+
const { ajax } = require("rxjs/ajax");
8+
9+
class JupyterServer {
10+
constructor(config = {}) {
11+
this.port = config.port || 9988;
12+
this.ip = config.ip || "127.0.0.1";
13+
this.scheme = config.scheme || "http";
14+
this.token = null;
15+
16+
// Launch the server from the directory of this script by default
17+
this.cwd = config.cwd || __dirname;
18+
19+
this.process = null;
20+
this.up = false;
21+
}
22+
23+
async start() {
24+
if (!this.token) {
25+
this.token = await genToken();
26+
}
27+
28+
this.process = child_process.spawn(
29+
"jupyter",
30+
[
31+
"notebook",
32+
"--no-browser",
33+
`--NotebookApp.token=${this.token}`,
34+
`--NotebookApp.disable_check_xsrf=True`,
35+
`--port=${this.port}`,
36+
`--ip=${this.ip}`
37+
],
38+
{ cwd: this.cwd }
39+
);
40+
41+
////// Refactor me later, streams are a bit messy with async await
42+
////// Let's use spawn-rx in the future and make some clean rxjs with timeouts
43+
this.process.stdout.on("data", data => {
44+
const s = data.toString();
45+
console.log(s);
46+
});
47+
this.process.stderr.on("data", data => {
48+
const s = data.toString();
49+
50+
console.error(s);
51+
if (s.includes("Jupyter Notebook is running at")) {
52+
this.up = true;
53+
}
54+
});
55+
this.process.stdout.on("end", data =>
56+
console.log("jupyter server terminated")
57+
);
58+
59+
await sleep(3000);
60+
61+
if (!this.up) {
62+
console.log("jupyter has not come up after 3 seconds, waiting 3 more");
63+
await sleep(3000);
64+
65+
if (!this.up) {
66+
throw new Error("jupyter has not come up after 6 seconds, bailing");
67+
}
68+
}
69+
}
70+
71+
async writeNotebook(path, notebook) {
72+
// Once https://github.com/nteract/nteract/pull/3651 is merged, we can use
73+
// rx-jupyter for writing a notebook to the contents API
74+
const xhr = await ajax({
75+
url: `${this.endpoint}/api/contents/${path}`,
76+
responseType: "json",
77+
createXHR: () => new XMLHttpRequest(),
78+
method: "PUT",
79+
body: {
80+
type: "notebook",
81+
content: notebook
82+
},
83+
headers: {
84+
"Content-Type": "application/json",
85+
Authorization: `token ${this.token}`
86+
}
87+
}).toPromise();
88+
89+
return xhr;
90+
}
91+
92+
shutdown() {
93+
this.process.kill();
94+
}
95+
96+
get endpoint() {
97+
return `${this.scheme}://${this.ip}:${this.port}`;
98+
}
99+
}
100+
101+
module.exports = {
102+
JupyterServer
103+
};

ci/jupyter_notebook_config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from bookstore import BookstoreContentsArchiver, BookstoreSettings
5+
6+
# jupyter config
7+
# At ~/.jupyter/jupyter_notebook_config.py for user installs
8+
# At __ for system installs
9+
c = get_config()
10+
11+
c.NotebookApp.contents_manager_class = BookstoreContentsArchiver
12+
13+
c.BookstoreSettings.workspace_prefix = "ci-workspace"
14+
15+
# If using minio for development
16+
c.BookstoreSettings.s3_endpoint_url = "http://127.0.0.1:9000"
17+
c.BookstoreSettings.s3_bucket = "bookstore"
18+
19+
# Straight out of `circleci/config.yml`
20+
c.BookstoreSettings.s3_access_key_id = "ONLY_ON_CIRCLE"
21+
c.BookstoreSettings.s3_secret_access_key = "CAN_WE_DO_THIS"

0 commit comments

Comments
 (0)