Skip to content

Commit 68a0278

Browse files
authored
Merge pull request #842 from krinsman/master
Reorganize init_tornado_application, create some configurables
2 parents f1c8cb1 + 722bff2 commit 68a0278

File tree

2 files changed

+195
-141
lines changed

2 files changed

+195
-141
lines changed

nbviewer/app.py

Lines changed: 190 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from jinja2 import Environment, FileSystemLoader
2727

28-
from traitlets import Unicode
28+
from traitlets import Unicode, Any, Set, default
2929
from traitlets.config import Application
3030

3131
from .handlers import init_handlers
@@ -44,6 +44,11 @@
4444
from .log import log_request
4545
from .utils import git_info, jupyter_info, url_path_join
4646

47+
try: # Python 3.8
48+
from functools import cached_property
49+
except ImportError:
50+
from .utils import cached_property
51+
4752
#-----------------------------------------------------------------------------
4853
# Code
4954
#-----------------------------------------------------------------------------
@@ -74,83 +79,96 @@ def nrfoot():
7479

7580
class NBViewer(Application):
7681

77-
config_file = Unicode('nbviewer_config.py', help="The config file to load").tag(config=True)
82+
name = Unicode('nbviewer')
7883

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)
8385

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'):
11097
log.app_log.info("Indexing notebooks")
11198
tcp_index = os.environ.get('NBINDEX_PORT')
11299
index_url = tcp_index.split('tcp://')[1]
113100
index_host, index_port = index_url.split(":")
114-
indexer = ElasticSearch(index_host, index_port)
115101
else:
116102
log.app_log.info("Not indexing notebooks")
117103
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]
119136
if options.no_cache:
120137
log.app_log.info("Not using cache")
121138
cache = MockCache()
122139
elif pylibmc and memcache_urls:
140+
# setup memcache
141+
mc_pool = ThreadPoolExecutor(options.mc_threads)
123142
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", "")
126145
if username and password:
127146
kwargs['binary'] = True
128147
kwargs['username'] = username
129148
kwargs['password'] = password
130149
log.app_log.info("Using SASL memcache")
131150
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)
135154
else:
136155
log.app_log.info("Using in-memory cache")
137156
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)
154172
env.filters['markdown'] = markdown.markdown
155173
try:
156174
git_data = git_info(here)
@@ -159,106 +177,137 @@ def init_tornado_application(self):
159177
git_data = {}
160178
else:
161179
git_data['msg'] = escape(git_data['msg'])
162-
163-
180+
164181
if options.no_cache:
165-
# force jinja to recompile template every time
182+
# force Jinja2 to recompile template every time
166183
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):
193190
fetch_kwargs = dict(connect_timeout=10,)
194191
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)
198193
log.app_log.info("Using web proxy {proxy_host}:{proxy_port}."
199194
"".format(**fetch_kwargs))
200-
195+
201196
if options.no_check_certificate:
202-
fetch_kwargs.update(dict(validate_cert=False))
203-
197+
fetch_kwargs.update(validate_cert=False)
204198
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)
208247

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
215258
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,
254264
)
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+
256308
if options.localfiles:
257309
log.app_log.warning("Serving local notebooks in %s, this can be a security risk", options.localfiles)
258310

259-
# handle handlers
260-
handlers = init_handlers(formats, options.providers, base_url, options.localfiles)
261-
262311
# create the app
263312
self.tornado_application = web.Application(handlers, debug=options.debug, **settings)
264313

0 commit comments

Comments
 (0)