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

Commit 753131a

Browse files
authored
Merge pull request #9 from nteract/config-for-all
setup config for use in archiver
2 parents 07d1e1d + 0a85011 commit 753131a

File tree

6 files changed

+89
-31
lines changed

6 files changed

+89
-31
lines changed

README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,16 @@ from bookstore import BookstoreContentsArchiver
4747

4848
c.NotebookApp.contents_manager_class = BookstoreContentsArchiver
4949

50-
c.Bookstore.workspace_prefix = "/workspace/kylek/notebooks"
51-
c.Bookstore.published_prefix = "/published/kylek/notebooks"
50+
c.BookstoreSettings.workspace_prefix = "/workspace/kylek/notebooks"
51+
c.BookstoreSettings.published_prefix = "/published/kylek/notebooks"
5252

5353
# Optional, in case you're using a different contents manager
5454
# This defaults to notebook.services.contents.manager.ContentsManager
55-
# c.bookstore.Archiver.underlying_contents_manager_class = ADifferentContentsManager
5655

57-
c.Bookstore.storage_class = BookstoreS3Settings
58-
59-
c.BookstoreS3Settings.bucket = "<bucket-name>"
56+
c.BookstoreSettings.s3_bucket = "<bucket-name>"
6057

6158
# Note: if bookstore is used from an EC2 instance with the right IAM role, you don't
6259
# have to specify these
63-
c.BookstoreS3Settings.access_key_id = <AWS Access Key ID / IAM Access Key ID>
64-
c.BookstoreS3Settings.secret_access_key = <AWS Secret Access Key / IAM Secret Access Key>
60+
c.BookstoreSettings.s3_access_key_id = <AWS Access Key ID / IAM Access Key ID>
61+
c.BookstoreSettings.s3_secret_access_key = <AWS Secret Access Key / IAM Secret Access Key>
6562
```

bookstore/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
from .jupyter_server_extension import load_jupyter_server_extension, _jupyter_server_extension_paths
77

88
from .archive import BookstoreContentsArchiver
9+
10+
from .bookstore_config import BookstoreSettings

bookstore/archive.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from s3contents.ipycompat import ContentsManager
2-
from s3contents.ipycompat import HasTraits, Unicode
1+
from notebook.services.contents.filemanager import FileContentsManager
2+
3+
import s3fs
34

45
from traitlets import (
6+
HasTraits,
57
Any,
68
Bool,
79
Dict,
@@ -12,16 +14,59 @@
1214
Unicode,
1315
validate,
1416
default,
17+
Instance
1518
)
1619

17-
class BookstoreContentsArchiver(ContentsManager, HasTraits):
20+
import json
21+
22+
from .bookstore_config import BookstoreSettings
23+
24+
class BookstoreContentsArchiver(FileContentsManager):
25+
"""
26+
Archives notebooks to S3 on save
1827
"""
19-
Archives contents via one ContentsManager and passes through to
20-
another ContentsManager.
2128

22-
Likely route:
29+
def __init__(self, *args, **kwargs):
30+
super(FileContentsManager, self).__init__(*args, **kwargs)
31+
# opt ourselves into being part of the Jupyter App that should have Bookstore Settings applied
32+
self.settings = BookstoreSettings(parent=self)
2333

24-
* Write directly to S3 on post_save_hook
34+
self.fs = s3fs.S3FileSystem(key=self.settings.s3_access_key_id,
35+
secret=self.settings.s3_secret_access_key,
36+
client_kwargs={
37+
"endpoint_url": self.settings.s3_endpoint_url,
38+
"region_name": self.settings.s3_region_name
39+
},
40+
config_kwargs={},
41+
s3_additional_kwargs={})
2542

26-
"""
27-
pass
43+
@property
44+
def delimiter(self):
45+
"""It's a slash! Normally this could be configurable. This leaves room for that later,
46+
keeping it centralized for now"""
47+
return "/"
48+
49+
@property
50+
def full_prefix(self):
51+
"""Full prefix: bucket + workspace prefix"""
52+
return self.delimiter.join([self.settings.s3_bucket, self.settings.workspace_prefix])
53+
54+
def s3_path(self, path):
55+
"""compute the s3 path based on the bucket, prefix, and the path to the notebook"""
56+
return self.delimiter.join([self.full_prefix, path])
57+
58+
def run_pre_save_hook(self, model, path, **kwargs):
59+
"""Store notebook to S3 when saves happen
60+
"""
61+
if model['type'] != 'notebook':
62+
return
63+
64+
# TODO: store the hash of the notebook to not write on every save
65+
notebook_contents = json.dumps(model['content'])
66+
67+
full_path = self.s3_path(path)
68+
69+
# write to S3
70+
# TODO: Write to S3 asynchronously to not block other server operations
71+
with self.fs.open(full_path, mode='wb') as f:
72+
f.write(notebook_contents.encode('utf-8'))

bookstore/bookstore_config.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,28 @@
1313

1414
from traitlets.config import LoggingConfigurable
1515

16-
class BookstoreS3Settings(LoggingConfigurable):
16+
class BookstoreSettings(LoggingConfigurable):
17+
"""The same settings to be shared across archival, publishing, and scheduling
18+
"""
19+
20+
workspace_prefix = Unicode("workspace", help="Prefix for the live workspace notebooks").tag(config=True)
21+
published_prefix = Unicode("published", help="Prefix for published notebooks").tag(config=True)
22+
23+
## S3 Settings for the S3 backed storage (other implementations can add on below)
1724
# Allowed to not set these as we can pick up IAM roles instead
18-
access_key_id = Unicode(
25+
s3_access_key_id = Unicode(
1926
help="S3/AWS access key ID", allow_none=True, default_value=None).tag(
2027
config=True, env="JPYNB_S3_ACCESS_KEY_ID")
21-
secret_access_key = Unicode(
28+
s3_secret_access_key = Unicode(
2229
help="S3/AWS secret access key", allow_none=True, default_value=None).tag(
2330
config=True, env="JPYNB_S3_SECRET_ACCESS_KEY")
2431

25-
endpoint_url = Unicode(
32+
s3_endpoint_url = Unicode(
2633
"https://s3.amazonaws.com", help="S3 endpoint URL").tag(
2734
config=True, env="JPYNB_S3_ENDPOINT_URL")
28-
region_name = Unicode(
35+
s3_region_name = Unicode(
2936
"us-east-1", help="Region name").tag(
3037
config=True, env="JPYNB_S3_REGION_NAME")
31-
bucket = Unicode(
32-
"notebooks", help="Bucket name to store notebooks").tag(
38+
s3_bucket = Unicode(
39+
"bookstore", help="Bucket name to store notebooks").tag(
3340
config=True, env="JPYNB_S3_BUCKET")
34-
35-
36-
class Bookstore(LoggingConfigurable):
37-
workspace_prefix = Unicode("workspace", help="Prefix for the live workspace notebooks").tag(config=True)
38-
published_prefix = Unicode("published", help="Prefix for published notebooks").tag(config=True)

jupyter_notebook_config.py.example

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@
33

44
print("Welcome to the bookstore 📚")
55

6-
from bookstore import BookstoreContentsArchiver
6+
from bookstore import BookstoreContentsArchiver, BookstoreSettings
77

8+
# jupyter config
9+
# At ~/.jupyter/jupyter_notebook_config.py for user installs
10+
# At __ for system installs
811
c = get_config()
912

1013
c.NotebookApp.contents_manager_class = BookstoreContentsArchiver
14+
15+
c.BookstoreSettings.workspace_prefix = "works"
16+
17+
# If using minio for development
18+
c.BookstoreSettings.s3_endpoint_url = "http://127.0.0.1:9000"
19+
c.BookstoreSettings.s3_bucket = "bookstore"
20+
c.BookstoreSettings.s3_access_key_id = "4MR9ON7H4UNVCT2LQTFX"
21+
c.BookstoreSettings.s3_secret_access_key = "o8CnAN5G9x87P9aLxSjohoSV0EsCLjksuY6wjK9N"

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
future
22
futures ; python_version < "3.0"
33
ipython >= 5.0
4-
s3contents
4+
notebook
5+
s3contents

0 commit comments

Comments
 (0)