Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support running .nbgitpuller.script.{init,update} on start #291

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ information.
pip install nbgitpuller
```

### Configuration

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.

## Example

This example shows how to use the [nbgitpuller link generator]
Expand Down
34 changes: 34 additions & 0 deletions jupyter_git_pull_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# May be set to a list of URLs described as Python regular expressions (using re.fullmatch())
# where it is permitted to autorun scripts from the pulled project as a pre-initialisation
# step.
#
# WARNING: Enable this only if you understand and accept the risks of AUTORUN.INF.
# ----
# c.NbGitPuller.autorun_allow = [
# r'https://github\.com/org/name\.git',
# r'https://github\.com/org-two/name-two\.git'
# ]
# ----
#
# To allow all sources (*not* recommended) use:
# ----
# c.NbGitPuller.autorun_allow = True
# ----
#
# The default is 'False' which means the autorun functionality is completely disabled
#c.NbGitPuller.autorun_allow = False

# List of scripts to search for when attempting to autorun. The first match will
# be run with a single argument of 'init' or 'update' depending on what nbgitpuller
# is doing.
# ----
# c.NbGitPuller.autorun_script = [
# '.nbgitpuller.script',
# '.different.script'
# ]
# ----
#
# The 'script' must be executable and when checked out on a 'exec' (ie. not a 'noexec') mountpoint
#
# The default is the empty list.
#c.NbGitPuller.autorun_script = []
78 changes: 2 additions & 76 deletions nbgitpuller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,8 @@
from .version import __version__ # noqa
from .pull import GitPuller # noqa
from jupyter_server.utils import url_path_join
from tornado.web import StaticFileHandler
import os
from .application import NbGitPuller


def _jupyter_server_extension_points():
"""
This function is detected by `notebook` and `jupyter_server` because they
are explicitly configured to inspect the nbgitpuller module for it. That
explicit configuration is passed via setup.py's declared data_files.

Returns a list of dictionaries with metadata describing where to find the
`_load_jupyter_server_extension` function.
"""
return [{
'module': 'nbgitpuller',
'app': NbGitPuller
}]


def _load_jupyter_server_extension(app):
"""
This function is a hook for `notebook` and `jupyter_server` that we use to
register additional endpoints to be handled by nbgitpuller.

Note that as this function is used as a hook for both notebook and
jupyter_server, the argument passed may be a NotebookApp or a ServerApp.

Related documentation:
- notebook: https://jupyter-notebook.readthedocs.io/en/stable/extending/handlers.htmland
- notebook: https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html#Example---Server-extension
- jupyter_server: https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html
"""
# identify base handler by app class
# must do this before importing from .handlers
from ._compat import get_base_handler

get_base_handler(app)

from .handlers import (
SyncHandler,
UIHandler,
LegacyInteractRedirectHandler,
LegacyGitSyncRedirectHandler,
)

web_app = app.web_app
base_url = url_path_join(web_app.settings['base_url'], 'git-pull')
handlers = [
(url_path_join(base_url, 'api'), SyncHandler),
(base_url, UIHandler),
(url_path_join(web_app.settings['base_url'], 'git-sync'), LegacyGitSyncRedirectHandler),
(url_path_join(web_app.settings['base_url'], 'interact'), LegacyInteractRedirectHandler),
(
url_path_join(base_url, 'static', '(.*)'),
StaticFileHandler,
{'path': os.path.join(os.path.dirname(__file__), 'static')}
)
]
# FIXME: See note on how to stop relying on settings to pass information:
# https://github.com/jupyterhub/nbgitpuller/pull/242#pullrequestreview-854968180
#
web_app.settings['nbapp'] = app
web_app.add_handlers('.*', handlers)


# For compatibility with both notebook and jupyter_server, we define
# _jupyter_server_extension_paths alongside _jupyter_server_extension_points.
#
# "..._paths" is used by notebook and still supported by jupyter_server as of
# jupyter_server 1.13.3, but was renamed to "..._points" in jupyter_server
# 1.0.0.
#
_jupyter_server_extension_paths = _jupyter_server_extension_points

# For compatibility with both notebook and jupyter_server, we define both
# load_jupyter_server_extension alongside _load_jupyter_server_extension.
#
# "load..." is used by notebook and "_load..." is used by jupyter_server.
#
load_jupyter_server_extension = _load_jupyter_server_extension
40 changes: 0 additions & 40 deletions nbgitpuller/_compat.py

This file was deleted.

58 changes: 58 additions & 0 deletions nbgitpuller/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from .version import __version__ # noqa
from .pull import GitPuller # noqa
from jupyter_server.extension.application import ExtensionApp
from traitlets import Bool, CRegExp, List, Unicode, Union
from traitlets.config import Configurable
import os


class NbGitPuller(ExtensionApp):
name = 'git-pull'
load_other_extensions = True

static_paths = [
os.path.join(os.path.dirname(__file__), 'static')
]

autorun_allow = Union(
[Bool(), List(CRegExp())],
default_value=False,
config=True,
help="""
List of URLs described as Python regular expressions (using re.fullmatch()) where
it is permitted to autorun scripts from the pulled project as a pre-initialisation
step. Enable this only if you understand and accept the risks of AUTORUN.INF.

When set to boolean True, all URLs are allowed, whilst False (default) autorun
is disabled completely.
"""
)

autorun_script = List(
Unicode(),
default_value=[],
config=True,
help="""
List of scripts to search for when attempting to autorun. The first match will
be run with a single argument of 'init' or 'update' depending on what nbgitpuller
is doing.

Enable this only if you understand and accept the risks of AUTORUN.INF.
"""
)

def initialize_handlers(self):
from .handlers import (
SyncHandler,
UIHandler,
LegacyInteractRedirectHandler,
LegacyGitSyncRedirectHandler,
)

# Extend the self.handlers trait
self.handlers.extend([
(rf'/{self.name}/api', SyncHandler),
(rf'/{self.name}', UIHandler),
(rf'/{self.name}/git-sync', LegacyGitSyncRedirectHandler),
(rf'/{self.name}/interact', LegacyInteractRedirectHandler),
])
11 changes: 6 additions & 5 deletions nbgitpuller/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@
import os
from queue import Queue, Empty
import jinja2
from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.extension.handler import ExtensionHandlerMixin

from .pull import GitPuller
from .version import __version__
from ._compat import get_base_handler

JupyterHandler = get_base_handler()


jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates')
),
)

class SyncHandler(JupyterHandler):
class SyncHandler(ExtensionHandlerMixin, JupyterHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.log.info(f'Config {self.config}')

# We use this lock to make sure that only one sync operation
# can be happening at a time. Git doesn't like concurrent use!
if 'git_lock' not in self.settings:
Expand Down Expand Up @@ -84,7 +85,7 @@ async def get(self):
self.set_header('content-type', 'text/event-stream')
self.set_header('cache-control', 'no-cache')

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

q = Queue()

Expand Down
39 changes: 35 additions & 4 deletions nbgitpuller/pull.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import subprocess
import logging
import time
Expand Down Expand Up @@ -69,18 +70,21 @@ def _depth_default(self):
where the GitPuller class hadn't been loaded already."""
return int(os.environ.get('NBGITPULLER_DEPTH', 1))

def __init__(self, git_url, repo_dir, **kwargs):
def __init__(self, git_url, repo_dir, branch, **kwargs):
assert git_url

self.git_url = git_url
self.branch_name = kwargs.pop("branch")
self.repo_dir = repo_dir
self.branch_name = branch

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

self.repo_dir = repo_dir
self.autorun_allow = kwargs.pop('autorun_allow', False)
self.autorun_script = kwargs.pop('autorun_script', [])

newargs = {k: v for k, v in kwargs.items() if v is not None}
super(GitPuller, self).__init__(**newargs)

Expand Down Expand Up @@ -143,6 +147,30 @@ def pull(self):
else:
yield from self.update()

def autorun(self, operation="method"):
"""
Search for and execute the autorun script.
"""

if not self.autorun_allow:
return
if not any(( re.fullmatch(pattern, self.git_url) for pattern in self.autorun_allow )):
logging.info('autorun skipped, URL does not match any rules')
return

script = next(( s for s in self.autorun_script if os.access(os.path.join(self.repo_dir, s), os.X_OK)), None)
if not script:
logging.info('autorun skipped, no matching (executable) script')
return

try:
for line in execute_cmd([ os.path.join(self.repo_dir, script), operation ], cwd=self.repo_dir, close_fds=True):
yield line
except subprocess.CalledProcessError:
m = f"Problem autorunning {script}"
logging.exception(m)
raise ValueError(m)

def initialize_repo(self):
"""
Clones repository
Expand All @@ -154,6 +182,7 @@ def initialize_repo(self):
clone_args.extend(['--branch', self.branch_name])
clone_args.extend(["--", self.git_url, self.repo_dir])
yield from execute_cmd(clone_args)
yield from self.autorun('init')
logging.info('Repo {} initialized'.format(self.repo_dir))

def reset_deleted_files(self):
Expand Down Expand Up @@ -343,6 +372,8 @@ def update(self):
yield from self.ensure_lock()
yield from self.merge()

yield from self.autorun('update')


def main():
"""
Expand All @@ -361,7 +392,7 @@ def main():
for line in GitPuller(
args.git_url,
args.repo_dir,
branch=args.branch_name if args.branch_name else None
args.branch_name if args.branch_name else None
).pull():
print(line)

Expand Down
2 changes: 1 addition & 1 deletion nbgitpuller/templates/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

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

{% block stylesheet %}
Expand Down
2 changes: 1 addition & 1 deletion tests/repohelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from uuid import uuid4

from packaging.version import Version as V
from nbgitpuller import GitPuller
from nbgitpuller.pull import GitPuller


class Repository:
Expand Down
Loading