Skip to content

Commit da48a81

Browse files
committed
autorun support
disabled by default, and configurable via trailets
1 parent f415428 commit da48a81

File tree

9 files changed

+140
-127
lines changed

9 files changed

+140
-127
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ information.
2222
pip install nbgitpuller
2323
```
2424

25+
### Configuration
26+
27+
Copy `jupyter_git_pull_config.py` to one of your Jupyter configuration paths (as determined from `jupyter --paths`) and edit it to meet your needs.
28+
2529
## Example
2630

2731
This example shows how to use the [nbgitpuller link generator]

jupyter_git_pull_config.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# May be set to a list of URLs described as Python regular expressions (using re.fullmatch())
2+
# where it is permitted to autorun scripts from the pulled project as a pre-initialisation
3+
# step.
4+
#
5+
# WARNING: Enable this only if you understand and accept the risks of AUTORUN.INF.
6+
# ----
7+
# c.NbGitPuller.autorun_allow = [
8+
# r'https://github\.com/org/name\.git',
9+
# r'https://github\.com/org-two/name-two\.git'
10+
# ]
11+
# ----
12+
#
13+
# To allow all sources (*not* recommended) use:
14+
# ----
15+
# c.NbGitPuller.autorun_allow = True
16+
# ----
17+
#
18+
# The default is 'False' which means the autorun functionality is completely disabled
19+
#c.NbGitPuller.autorun_allow = False
20+
21+
# List of scripts to search for when attempting to autorun. The first match will
22+
# be run with a single argument of 'init' or 'update' depending on what nbgitpuller
23+
# is doing.
24+
# ----
25+
# c.NbGitPuller.autorun_script = [
26+
# '.nbgitpuller.script',
27+
# '.different.script'
28+
# ]
29+
# ----
30+
#
31+
# The 'script' must be executable and when checked out on a 'exec' (ie. not a 'noexec') mountpoint
32+
#
33+
# The default is the empty list.
34+
#c.NbGitPuller.autorun_script = []

nbgitpuller/__init__.py

Lines changed: 2 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,8 @@
1-
from .version import __version__ # noqa
2-
from .pull import GitPuller # noqa
3-
from jupyter_server.utils import url_path_join
4-
from tornado.web import StaticFileHandler
5-
import os
1+
from .application import NbGitPuller
62

73

84
def _jupyter_server_extension_points():
9-
"""
10-
This function is detected by `notebook` and `jupyter_server` because they
11-
are explicitly configured to inspect the nbgitpuller module for it. That
12-
explicit configuration is passed via setup.py's declared data_files.
13-
14-
Returns a list of dictionaries with metadata describing where to find the
15-
`_load_jupyter_server_extension` function.
16-
"""
175
return [{
186
'module': 'nbgitpuller',
7+
'app': NbGitPuller
198
}]
20-
21-
22-
def _load_jupyter_server_extension(app):
23-
"""
24-
This function is a hook for `notebook` and `jupyter_server` that we use to
25-
register additional endpoints to be handled by nbgitpuller.
26-
27-
Note that as this function is used as a hook for both notebook and
28-
jupyter_server, the argument passed may be a NotebookApp or a ServerApp.
29-
30-
Related documentation:
31-
- notebook: https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.htmland
32-
- notebook: https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html#Example---Server-extension
33-
- jupyter_server: https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html
34-
"""
35-
# identify base handler by app class
36-
# must do this before importing from .handlers
37-
from ._compat import get_base_handler
38-
39-
get_base_handler(app)
40-
41-
from .handlers import (
42-
SyncHandler,
43-
UIHandler,
44-
LegacyInteractRedirectHandler,
45-
LegacyGitSyncRedirectHandler,
46-
)
47-
48-
web_app = app.web_app
49-
base_url = url_path_join(web_app.settings['base_url'], 'git-pull')
50-
handlers = [
51-
(url_path_join(base_url, 'api'), SyncHandler),
52-
(base_url, UIHandler),
53-
(url_path_join(web_app.settings['base_url'], 'git-sync'), LegacyGitSyncRedirectHandler),
54-
(url_path_join(web_app.settings['base_url'], 'interact'), LegacyInteractRedirectHandler),
55-
(
56-
url_path_join(base_url, 'static', '(.*)'),
57-
StaticFileHandler,
58-
{'path': os.path.join(os.path.dirname(__file__), 'static')}
59-
)
60-
]
61-
# FIXME: See note on how to stop relying on settings to pass information:
62-
# https://github.com/jupyterhub/nbgitpuller/pull/242#pullrequestreview-854968180
63-
#
64-
web_app.settings['nbapp'] = app
65-
web_app.add_handlers('.*', handlers)
66-
67-
68-
# For compatibility with both notebook and jupyter_server, we define
69-
# _jupyter_server_extension_paths alongside _jupyter_server_extension_points.
70-
#
71-
# "..._paths" is used by notebook and still supported by jupyter_server as of
72-
# jupyter_server 1.13.3, but was renamed to "..._points" in jupyter_server
73-
# 1.0.0.
74-
#
75-
_jupyter_server_extension_paths = _jupyter_server_extension_points
76-
77-
# For compatibility with both notebook and jupyter_server, we define both
78-
# load_jupyter_server_extension alongside _load_jupyter_server_extension.
79-
#
80-
# "load..." is used by notebook and "_load..." is used by jupyter_server.
81-
#
82-
load_jupyter_server_extension = _load_jupyter_server_extension

nbgitpuller/_compat.py

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

nbgitpuller/application.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from .version import __version__ # noqa
2+
from .pull import GitPuller # noqa
3+
from jupyter_server.extension.application import ExtensionApp
4+
from traitlets import Bool, CRegExp, List, Unicode, Union
5+
from traitlets.config import Configurable
6+
import os
7+
8+
9+
class NbGitPuller(ExtensionApp):
10+
name = 'git-pull'
11+
load_other_extensions = True
12+
13+
static_paths = [
14+
os.path.join(os.path.dirname(__file__), 'static')
15+
]
16+
17+
autorun_allow = Union(
18+
[Bool(), List(CRegExp())],
19+
default_value=False,
20+
config=True,
21+
help="""
22+
List of URLs described as Python regular expressions (using re.fullmatch()) where
23+
it is permitted to autorun scripts from the pulled project as a pre-initialisation
24+
step. Enable this only if you understand and accept the risks of AUTORUN.INF.
25+
26+
When set to boolean True, all URLs are allowed, whilst False (default) autorun
27+
is disabled completely.
28+
"""
29+
)
30+
31+
autorun_script = List(
32+
Unicode(),
33+
default_value=[],
34+
config=True,
35+
help="""
36+
List of scripts to search for when attempting to autorun. The first match will
37+
be run with a single argument of 'init' or 'update' depending on what nbgitpuller
38+
is doing.
39+
40+
Enable this only if you understand and accept the risks of AUTORUN.INF.
41+
"""
42+
)
43+
44+
def initialize_handlers(self):
45+
from .handlers import (
46+
SyncHandler,
47+
UIHandler,
48+
LegacyInteractRedirectHandler,
49+
LegacyGitSyncRedirectHandler,
50+
)
51+
52+
# Extend the self.handlers trait
53+
self.handlers.extend([
54+
(rf'/{self.name}/api', SyncHandler),
55+
(rf'/{self.name}', UIHandler),
56+
(rf'/{self.name}/git-sync', LegacyGitSyncRedirectHandler),
57+
(rf'/{self.name}/interact', LegacyInteractRedirectHandler),
58+
])

nbgitpuller/handlers.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,24 @@
77
import os
88
from queue import Queue, Empty
99
import jinja2
10+
from jupyter_server.base.handlers import JupyterHandler
11+
from jupyter_server.extension.handler import ExtensionHandlerMixin
1012

1113
from .pull import GitPuller
1214
from .version import __version__
13-
from ._compat import get_base_handler
14-
15-
JupyterHandler = get_base_handler()
1615

1716

1817
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(
1918
os.path.join(os.path.dirname(__file__), 'templates')
2019
),
2120
)
2221

23-
class SyncHandler(JupyterHandler):
22+
class SyncHandler(ExtensionHandlerMixin, JupyterHandler):
2423
def __init__(self, *args, **kwargs):
2524
super().__init__(*args, **kwargs)
2625

26+
self.log.info(f'Config {self.config}')
27+
2728
# We use this lock to make sure that only one sync operation
2829
# can be happening at a time. Git doesn't like concurrent use!
2930
if 'git_lock' not in self.settings:
@@ -84,7 +85,7 @@ async def get(self):
8485
self.set_header('content-type', 'text/event-stream')
8586
self.set_header('cache-control', 'no-cache')
8687

87-
gp = GitPuller(repo, repo_dir, branch=branch, depth=depth, parent=self.settings['nbapp'])
88+
gp = GitPuller(repo, repo_dir, branch, depth=depth, **self.config)
8889

8990
q = Queue()
9091

nbgitpuller/pull.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import subprocess
34
import logging
45
import time
@@ -69,18 +70,21 @@ def _depth_default(self):
6970
where the GitPuller class hadn't been loaded already."""
7071
return int(os.environ.get('NBGITPULLER_DEPTH', 1))
7172

72-
def __init__(self, git_url, repo_dir, **kwargs):
73+
def __init__(self, git_url, repo_dir, branch, **kwargs):
7374
assert git_url
7475

7576
self.git_url = git_url
76-
self.branch_name = kwargs.pop("branch")
77+
self.repo_dir = repo_dir
78+
self.branch_name = branch
7779

7880
if self.branch_name is None:
7981
self.branch_name = self.resolve_default_branch()
8082
elif not self.branch_exists(self.branch_name):
8183
raise ValueError(f"Branch: {self.branch_name} -- not found in repo: {self.git_url}")
8284

83-
self.repo_dir = repo_dir
85+
self.autorun_allow = kwargs.pop('autorun_allow', False)
86+
self.autorun_script = kwargs.pop('autorun_script', [])
87+
8488
newargs = {k: v for k, v in kwargs.items() if v is not None}
8589
super(GitPuller, self).__init__(**newargs)
8690

@@ -143,6 +147,30 @@ def pull(self):
143147
else:
144148
yield from self.update()
145149

150+
def autorun(self, operation="method"):
151+
"""
152+
Search for and execute the autorun script.
153+
"""
154+
155+
if not self.autorun_allow:
156+
return
157+
if not any(( re.fullmatch(pattern, self.git_url) for pattern in self.autorun_allow )):
158+
logging.info('autorun skipped, URL does not match any rules')
159+
return
160+
161+
script = next(( s for s in self.autorun_script if os.path.exists(os.path.join(self.repo_dir, s)) ), None)
162+
if not script:
163+
logging.info('autorun skipped, no matching script')
164+
return
165+
166+
try:
167+
for line in execute_cmd([ os.path.join(self.repo_dir, script), operation ], cwd=self.repo_dir, close_fds=True):
168+
yield line
169+
except subprocess.CalledProcessError:
170+
m = f"Problem autorunning {script}"
171+
logging.exception(m)
172+
raise ValueError(m)
173+
146174
def initialize_repo(self):
147175
"""
148176
Clones repository
@@ -154,6 +182,7 @@ def initialize_repo(self):
154182
clone_args.extend(['--branch', self.branch_name])
155183
clone_args.extend(["--", self.git_url, self.repo_dir])
156184
yield from execute_cmd(clone_args)
185+
yield from self.autorun('init')
157186
logging.info('Repo {} initialized'.format(self.repo_dir))
158187

159188
def reset_deleted_files(self):
@@ -343,6 +372,7 @@ def update(self):
343372
yield from self.ensure_lock()
344373
yield from self.merge()
345374

375+
yield from self.autorun('update')
346376

347377
def main():
348378
"""
@@ -361,7 +391,7 @@ def main():
361391
for line in GitPuller(
362392
args.git_url,
363393
args.repo_dir,
364-
branch=args.branch_name if args.branch_name else None
394+
args.branch_name if args.branch_name else None
365395
).pull():
366396
print(line)
367397

nbgitpuller/templates/status.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
{% block script %}
3737
{{super()}}
38-
<script src="{{ base_url }}git-pull/static/dist/bundle.js"></script>
38+
<script src="{{ base_url }}/static/git-pull/dist/bundle.js"></script>
3939
{% endblock %}
4040

4141
{% block stylesheet %}

tests/repohelpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from uuid import uuid4
99

1010
from packaging.version import Version as V
11-
from nbgitpuller import GitPuller
11+
from nbgitpuller.pull import GitPuller
1212

1313

1414
class Repository:

0 commit comments

Comments
 (0)