25
25
26
26
from jinja2 import Environment , FileSystemLoader
27
27
28
- from traitlets import Unicode
28
+ from traitlets import Unicode , Any , Set , default
29
29
from traitlets .config import Application
30
30
31
31
from .handlers import init_handlers
44
44
from .log import log_request
45
45
from .utils import git_info , jupyter_info , url_path_join
46
46
47
+ try : # Python 3.8
48
+ from functools import cached_property
49
+ except ImportError :
50
+ from .utils import cached_property
51
+
47
52
#-----------------------------------------------------------------------------
48
53
# Code
49
54
#-----------------------------------------------------------------------------
@@ -74,83 +79,96 @@ def nrfoot():
74
79
75
80
class NBViewer (Application ):
76
81
77
- config_file = Unicode ('nbviewer_config.py' , help = "The config file to load" ). tag ( config = True )
82
+ name = Unicode ('nbviewer' )
78
83
79
- def init_tornado_application (self ):
80
- # NBConvert config
81
- self .config .NbconvertApp .fileext = 'html'
82
- self .config .CSSHTMLHeaderTransformer .enabled = False
84
+ config_file = Unicode ('nbviewer_config.py' , help = "The config file to load" ).tag (config = True )
83
85
84
- # DEBUG env implies both autoreload and log-level
85
- if os .environ .get ("DEBUG" ):
86
- options .debug = True
87
- logging .getLogger ().setLevel (logging .DEBUG )
88
-
89
- # setup memcache
90
- mc_pool = ThreadPoolExecutor (options .mc_threads )
91
-
92
- # setup formats
93
- formats = configure_formats (options , self .config , log .app_log )
94
-
95
- if options .processes :
96
- pool = ProcessPoolExecutor (options .processes )
97
- else :
98
- pool = ThreadPoolExecutor (options .threads )
99
-
100
- memcache_urls = os .environ .get ('MEMCACHIER_SERVERS' ,
101
- os .environ .get ('MEMCACHE_SERVERS' )
102
- )
103
-
104
- # Handle linked Docker containers
105
- if (os .environ .get ('NBCACHE_PORT' )):
106
- tcp_memcache = os .environ .get ('NBCACHE_PORT' )
107
- memcache_urls = tcp_memcache .split ('tcp://' )[1 ]
108
-
109
- if (os .environ .get ('NBINDEX_PORT' )):
86
+ url_handler = Unicode (default_value = "nbviewer.providers.url.handlers.URLHandler" , help = "The Tornado handler to use for viewing notebooks accessed via URL" ).tag (config = True )
87
+ local_handler = Unicode (default_value = "nbviewer.providers.local.handlers.LocalFileHandler" , help = "The Tornado handler to use for viewing notebooks found on a local filesystem" ).tag (config = True )
88
+ github_blob_handler = Unicode (default_value = "nbviewer.providers.github.handlers.GitHubBlobHandler" , help = "The Tornado handler to use for viewing notebooks stored as blobs on GitHub" ).tag (config = True )
89
+ github_tree_handler = Unicode (default_value = "nbviewer.providers.github.handlers.GitHubTreeHandler" , help = "The Tornado handler to use for viewing directory trees on GitHub" ).tag (config = True )
90
+ gist_handler = Unicode (default_value = "nbviewer.providers.gist.handlers.GistHandler" , help = "The Tornado handler to use for viewing notebooks stored as GitHub Gists" ).tag (config = True )
91
+ user_gists_handler = Unicode (default_value = "nbviewer.providers.gist.handlers.UserGistsHandler" , help = "The Tornado handler to use for viewing directory containing all of a user's Gists" ).tag (config = True )
92
+
93
+ index = Any ().tag (config = True )
94
+ @default ('index' )
95
+ def _load_index (self ):
96
+ if os .environ .get ('NBINDEX_PORT' ):
110
97
log .app_log .info ("Indexing notebooks" )
111
98
tcp_index = os .environ .get ('NBINDEX_PORT' )
112
99
index_url = tcp_index .split ('tcp://' )[1 ]
113
100
index_host , index_port = index_url .split (":" )
114
- indexer = ElasticSearch (index_host , index_port )
115
101
else :
116
102
log .app_log .info ("Not indexing notebooks" )
117
103
indexer = NoSearch ()
118
-
104
+ return indexer
105
+
106
+ # cache frontpage links for the maximum allowed time
107
+ max_cache_uris = Set ().tag (config = True )
108
+ @default ('max_cache_uris' )
109
+ def _load_max_cache_uris (self ):
110
+ max_cache_uris = {'' }
111
+ for section in self .frontpage_setup ['sections' ]:
112
+ for link in section ['links' ]:
113
+ max_cache_uris .add ('/' + link ['target' ])
114
+ return max_cache_uris
115
+
116
+ static_path = Unicode (default_value = pjoin (here , 'static' )).tag (config = True )
117
+
118
+ static_url_prefix = Unicode ().tag (config = True )
119
+ @default ('static_url_prefix' )
120
+ def _load_static_url_prefix (self ):
121
+ return url_path_join (self .base_url , '/static/' )
122
+
123
+ @cached_property
124
+ def base_url (self ):
125
+ # prefer the JupyterHub defined service prefix over the CLI
126
+ base_url = os .getenv ("JUPYTERHUB_SERVICE_PREFIX" , options .base_url )
127
+ return base_url
128
+
129
+ @cached_property
130
+ def cache (self ):
131
+ memcache_urls = os .environ .get ('MEMCACHIER_SERVERS' , os .environ .get ('MEMCACHE_SERVERS' ))
132
+ # Handle linked Docker containers
133
+ if os .environ .get ('NBCACHE_PORT' ):
134
+ tcp_memcache = os .environ .get ('NBCACHE_PORT' )
135
+ memcache_urls = tcp_memcache .split ('tcp://' )[1 ]
119
136
if options .no_cache :
120
137
log .app_log .info ("Not using cache" )
121
138
cache = MockCache ()
122
139
elif pylibmc and memcache_urls :
140
+ # setup memcache
141
+ mc_pool = ThreadPoolExecutor (options .mc_threads )
123
142
kwargs = dict (pool = mc_pool )
124
- username = os .environ .get (' MEMCACHIER_USERNAME' , '' )
125
- password = os .environ .get (' MEMCACHIER_PASSWORD' , '' )
143
+ username = os .environ .get (" MEMCACHIER_USERNAME" , "" )
144
+ password = os .environ .get (" MEMCACHIER_PASSWORD" , "" )
126
145
if username and password :
127
146
kwargs ['binary' ] = True
128
147
kwargs ['username' ] = username
129
148
kwargs ['password' ] = password
130
149
log .app_log .info ("Using SASL memcache" )
131
150
else :
132
- log .app_log .info ("Using plain memecache " )
133
-
134
- cache = AsyncMultipartMemcache (memcache_urls .split (',' ), ** kwargs )
151
+ log .app_log .info ("Using plain memcache " )
152
+
153
+ cache = AsyncMultiPartMemcache (memcache_urls .split (',' ), ** kwargs )
135
154
else :
136
155
log .app_log .info ("Using in-memory cache" )
137
156
cache = DummyAsyncCache ()
138
-
139
- # setup tornado handlers and settings
140
-
141
- template_paths = pjoin (here , 'templates' )
142
-
143
- if options .template_path is not None :
144
- log .app_log .info ("Using custom template path {}" .format (
145
- options .template_path )
146
- )
147
- template_paths = [options .template_path , template_paths ]
148
-
149
- static_path = pjoin (here , 'static' )
150
- env = Environment (
151
- loader = FileSystemLoader (template_paths ),
152
- autoescape = True
153
- )
157
+
158
+ return cache
159
+
160
+ # for some reason this needs to be a computed property,
161
+ # and not a traitlets Any(), otherwise nbviewer won't run
162
+ @cached_property
163
+ def client (self ):
164
+ AsyncHTTPClient .configure (HTTPClientClass )
165
+ client = AsyncHTTPClient ()
166
+ client .cache = self .cache
167
+ return client
168
+
169
+ @cached_property
170
+ def env (self ):
171
+ env = Environment (loader = FileSystemLoader (self .template_paths ), autoescape = True )
154
172
env .filters ['markdown' ] = markdown .markdown
155
173
try :
156
174
git_data = git_info (here )
@@ -159,106 +177,137 @@ def init_tornado_application(self):
159
177
git_data = {}
160
178
else :
161
179
git_data ['msg' ] = escape (git_data ['msg' ])
162
-
163
-
180
+
164
181
if options .no_cache :
165
- # force jinja to recompile template every time
182
+ # force Jinja2 to recompile template every time
166
183
env .globals .update (cache_size = 0 )
167
- env .globals .update (nrhead = nrhead , nrfoot = nrfoot , git_data = git_data ,
168
- jupyter_info = jupyter_info (), len = len ,
169
- )
170
- AsyncHTTPClient .configure (HTTPClientClass )
171
- client = AsyncHTTPClient ()
172
- client .cache = cache
173
-
174
- # load frontpage sections
175
- with io .open (options .frontpage , 'r' ) as f :
176
- frontpage_setup = json .load (f )
177
- # check if the json has a 'sections' field, otherwise assume it is
178
- # just a list of sessions, and provide the defaults for the other
179
- # fields
180
- if 'sections' not in frontpage_setup :
181
- frontpage_setup = {'title' : 'nbviewer' ,
182
- 'subtitle' :
183
- 'A simple way to share Jupyter Notebooks' ,
184
- 'show_input' : True ,
185
- 'sections' : frontpage_setup }
186
-
187
- # cache frontpage links for the maximum allowed time
188
- max_cache_uris = {'' }
189
- for section in frontpage_setup ['sections' ]:
190
- for link in section ['links' ]:
191
- max_cache_uris .add ('/' + link ['target' ])
192
-
184
+ env .globals .update (nrhead = nrhead , nrfoot = nrfoot , git_data = git_data , jupyter_info = jupyter_info (), len = len )
185
+
186
+ return env
187
+
188
+ @cached_property
189
+ def fetch_kwargs (self ):
193
190
fetch_kwargs = dict (connect_timeout = 10 ,)
194
191
if options .proxy_host :
195
- fetch_kwargs .update (dict (proxy_host = options .proxy_host ,
196
- proxy_port = options .proxy_port ))
197
-
192
+ fetch_kwargs .update (proxy_host = options .proxy_host , proxy_port = options .proxy_port )
198
193
log .app_log .info ("Using web proxy {proxy_host}:{proxy_port}."
199
194
"" .format (** fetch_kwargs ))
200
-
195
+
201
196
if options .no_check_certificate :
202
- fetch_kwargs .update (dict (validate_cert = False ))
203
-
197
+ fetch_kwargs .update (validate_cert = False )
204
198
log .app_log .info ("Not validating SSL certificates" )
205
-
206
- # prefer the jhub defined service prefix over the CLI
207
- base_url = os .getenv ('JUPYTERHUB_SERVICE_PREFIX' , options .base_url )
199
+
200
+ return fetch_kwargs
201
+
202
+ @cached_property
203
+ def formats (self ):
204
+ formats = configure_formats (options , self .config , log .app_log )
205
+ return formats
206
+
207
+ # load frontpage sections
208
+ @cached_property
209
+ def frontpage_setup (self ):
210
+ with io .open (options .frontpage , 'r' ) as f :
211
+ frontpage_setup = json .load (f )
212
+ # check if the JSON has a 'sections' field, otherwise assume it is just a list of sessions,
213
+ # and provide the defaults of the other fields
214
+ if 'sections' not in frontpage_setup :
215
+ frontpage_setup = {
216
+ 'title' :'nbviewer' , 'subtitle' :'A simple way to share Jupyter notebooks' ,
217
+ 'show_input' :True , 'sections' :frontpage_setup
218
+ }
219
+ return frontpage_setup
220
+
221
+ @cached_property
222
+ def pool (self ):
223
+ if options .processes :
224
+ pool = ProcessPoolExecutor (options .processes )
225
+ else :
226
+ pool = ThreadPoolExecutor (options .threads )
227
+ return pool
228
+
229
+ @cached_property
230
+ def rate_limiter (self ):
231
+ rate_limiter = RateLimiter (limit = options .rate_limit , interval = options .rate_limit_interval , cache = self .cache )
232
+ return rate_limiter
233
+
234
+ @cached_property
235
+ def template_paths (self ):
236
+ template_paths = pjoin (here , 'templates' )
237
+ if options .template_path is not None :
238
+ log .app_log .info ("Using custom template path {}" .format (options .template_path ))
239
+ template_paths = [options .template_path , template_paths ]
240
+
241
+ return template_paths
242
+
243
+
244
+ def init_tornado_application (self ):
245
+ # handle handlers
246
+ handlers = init_handlers (self .formats , options .providers , self .base_url , options .localfiles )
208
247
209
- rate_limiter = RateLimiter (
210
- limit = options .rate_limit ,
211
- interval = options .rate_limit_interval ,
212
- cache = cache ,
213
- )
214
-
248
+ # NBConvert config
249
+ self .config .NbconvertApp .fileext = 'html'
250
+ self .config .CSSHTMLHeaderTransformer .enabled = False
251
+
252
+ # DEBUG env implies both autoreload and log-level
253
+ if os .environ .get ("DEBUG" ):
254
+ options .debug = True
255
+ logging .getLogger ().setLevel (logging .DEBUG )
256
+
257
+ # input traitlets to settings
215
258
settings = dict (
216
- log_function = log_request ,
217
- jinja2_env = env ,
218
- static_path = static_path ,
219
- static_url_prefix = url_path_join (base_url , '/static/' ),
220
- client = client ,
221
- formats = formats ,
222
- default_format = options .default_format ,
223
- providers = options .providers ,
224
- provider_rewrites = options .provider_rewrites ,
225
- config = self .config ,
226
- index = indexer ,
227
- cache = cache ,
228
- cache_expiry_min = options .cache_expiry_min ,
229
- cache_expiry_max = options .cache_expiry_max ,
230
- max_cache_uris = max_cache_uris ,
231
- frontpage_setup = frontpage_setup ,
232
- pool = pool ,
233
- gzip = True ,
234
- render_timeout = options .render_timeout ,
235
- localfile_path = os .path .abspath (options .localfiles ),
236
- localfile_follow_symlinks = options .localfile_follow_symlinks ,
237
- localfile_any_user = options .localfile_any_user ,
238
- fetch_kwargs = fetch_kwargs ,
239
- mathjax_url = options .mathjax_url ,
240
- rate_limiter = rate_limiter ,
241
- statsd_host = options .statsd_host ,
242
- statsd_port = options .statsd_port ,
243
- statsd_prefix = options .statsd_prefix ,
244
- base_url = base_url ,
245
- google_analytics_id = os .getenv ('GOOGLE_ANALYTICS_ID' ),
246
- hub_api_token = os .getenv ('JUPYTERHUB_API_TOKEN' ),
247
- hub_api_url = os .getenv ('JUPYTERHUB_API_URL' ),
248
- hub_base_url = os .getenv ('JUPYTERHUB_BASE_URL' ),
249
- ipywidgets_base_url = options .ipywidgets_base_url ,
250
- jupyter_widgets_html_manager_version = options .jupyter_widgets_html_manager_version ,
251
- jupyter_js_widgets_version = options .jupyter_js_widgets_version ,
252
- content_security_policy = options .content_security_policy ,
253
- binder_base_url = options .binder_base_url ,
259
+ config = self .config ,
260
+ index = self .index ,
261
+ max_cache_uris = self .max_cache_uris ,
262
+ static_path = self .static_path ,
263
+ static_url_prefix = self .static_url_prefix ,
254
264
)
255
-
265
+ # input computed properties to settings
266
+ settings .update (
267
+ base_url = self .base_url ,
268
+ cache = self .cache ,
269
+ client = self .client ,
270
+ fetch_kwargs = self .fetch_kwargs ,
271
+ formats = self .formats ,
272
+ frontpage_setup = self .frontpage_setup ,
273
+ jinja2_env = self .env ,
274
+ pool = self .pool ,
275
+ rate_limiter = self .rate_limiter ,
276
+ )
277
+ # input settings from CLI options
278
+ settings .update (
279
+ binder_base_url = options .binder_base_url ,
280
+ cache_expiry_max = options .cache_expiry_max ,
281
+ cache_expiry_min = options .cache_expiry_min ,
282
+ content_security_policy = options .content_security_policy ,
283
+ default_format = options .default_format ,
284
+ ipywidgets_base_url = options .ipywidgets_base_url ,
285
+ jupyter_js_widgets_version = options .jupyter_js_widgets_version ,
286
+ jupyter_widgets_html_manager_version = options .jupyter_widgets_html_manager_version ,
287
+ localfile_any_user = options .localfile_any_user ,
288
+ localfile_follow_symlinks = options .localfile_follow_symlinks ,
289
+ localfile_path = os .path .abspath (options .localfiles ),
290
+ mathjax_url = options .mathjax_url ,
291
+ provider_rewrites = options .provider_rewrites ,
292
+ providers = options .providers ,
293
+ render_timeout = options .render_timeout ,
294
+ statsd_host = options .statsd_host ,
295
+ statsd_port = options .statsd_port ,
296
+ statsd_prefix = options .statsd_prefix ,
297
+ )
298
+ # additional settings
299
+ settings .update (
300
+ google_analytics_id = os .getenv ('GOOGLE_ANALYTICS_ID' ),
301
+ gzip = True ,
302
+ hub_api_token = os .getenv ('JUPYTERHUB_API_TOKEN' ),
303
+ hub_api_url = os .getenv ('JUPYTERHUB_API_URL' ),
304
+ hub_base_url = os .getenv ('JUPYTERHUB_BASE_URL' ),
305
+ log_function = log_request ,
306
+ )
307
+
256
308
if options .localfiles :
257
309
log .app_log .warning ("Serving local notebooks in %s, this can be a security risk" , options .localfiles )
258
310
259
- # handle handlers
260
- handlers = init_handlers (formats , options .providers , base_url , options .localfiles )
261
-
262
311
# create the app
263
312
self .tornado_application = web .Application (handlers , debug = options .debug , ** settings )
264
313
0 commit comments