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

[wip] Add binder #44

Open
wants to merge 11 commits into
base: master
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
27 changes: 27 additions & 0 deletions .binder/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: pull-requests

channels:
- conda-forge
- nodefaults

dependencies:
- git
- nodejs >=14,<15
- jupyterlab >=3,<4
- python >=3.7,<3.9
# gitlab
- diff-match-patch
# test
- codecov
- mock >=4.0.0
- pytest
- pytest-asyncio
- pytest-cov
- pytest-tornasync
# not-yet-on-conda-forge
- pip
# binder demo stuff
- nbgitpuller
- jupyterlab-tour
- pip:
- jupyterlab-git >=0.30.0b3,<0.40.0a0
15 changes: 15 additions & 0 deletions .binder/jupyter_config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"LabApp": {
"tornado_settings": {
"page_config_data": {
"buildCheck": false,
"buildAvailable": false
}
}
},
"PRConfig": {
"provider": "github-anonymous",
"owner": "jupyterlab",
"repo": "jupyterlab"
}
}
21 changes: 21 additions & 0 deletions .binder/postBuild
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -eux

jlpm --prefer-optional --ignore-offline
jlpm build:lib
jlpm build:labextension:dev

python -m pip install -e . --ignore-installed --no-deps

jupyter labextension develop . --overwrite
jupyter server extension enable --py jupyterlab_pullrequests --sys-prefix
jupyter serverextension enable --py jupyterlab_pullrequests --sys-prefix

jupyter server extension list
jupyter serverextension list
jupyter labextension list

# copy custom jupyter config out of binder
cp .binder/jupyter_config.example.json ./jupyter_config.json
cp .binder/jupyter_config.example.json ./jupyter_notebook_config.json
cp .binder/jupyter_config.example.json ./jupyter_server_config.json
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,5 @@ dmypy.json

.DS_Store
coverage/

jupyter_config.json
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
[![NPM Version](https://img.shields.io/npm/v/@jupyterlab/pullrequests.svg)](https://www.npmjs.com/package/@jupyterlab/pullrequests)
[![Pypi Version](https://img.shields.io/pypi/v/jupyterlab-pullrequests.svg)](https://pypi.org/project/jupyterlab-pullrequests/)
[![Conda Version](https://img.shields.io/conda/vn/conda-forge/jupyterlab-pullrequests.svg)](https://anaconda.org/conda-forge/jupyterlab-pullrequests)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/pull-requests/HEAD?urlpath=lab)


A JupyterLab extension for reviewing pull requests.

Expand All @@ -14,7 +16,7 @@ For now, it supports GitHub and GitLab providers.
## Prerequisites

- JupyterLab 3.x
- for JupyterLab 2.x, see the [`2.x` branch](https://github.com/jupyterlab/pull-requests/tree/2.x)
- for JupyterLab 2.x, see the [`2.x` branch](https://github.com/jupyterlab/pull-requests/tree/2.x)
- [jupyterlab-git](https://github.com/jupyterlab/jupyterlab-git) >=0.30.0

> For GitLab, you will need also `diff-match-patch`
Expand Down Expand Up @@ -51,6 +53,35 @@ Or with conda:
conda install -c conda-forge diff-match-patch
```

### 1.5. Anonymous GitHub

The `github-anonymous` is good for quick demos, as it doesn't require an _access token_.
Combined with some binder configuration, this can be tailored for low-barrier, read-only
access to PR discussions on e.g. Binder.

The limitations:
- at best, rate-limited to 60 requests on e.g. Binder
- it cannot use the search APIs, and

Because of these, it requires an `owner` and `repo` to list (the first) page of
a particular repo's PRs:

```python
c.PRConfig.provider = 'github-anonymous'
c.PRConfig.owner = 'jupyterlab'
c.PRConfig.repo = 'pull-requests
```

> TODO: move to a deeper reference section
```http
GET https://api.github.com/repos/jupyterlab/pull-requests/pulls?state=all

x-ratelimit-limit: 60
x-ratelimit-remaining: 52
x-ratelimit-reset: 1617905825
x-ratelimit-used: 8
```

### 2. Getting your access token

For GitHub, the documentation is [there](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). The token scope must be **repo**.
Expand Down
12 changes: 10 additions & 2 deletions jupyterlab_pullrequests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def _load_jupyter_server_extension(server_app):
from .base import PRConfig
from .handlers import setup_handlers

log = server_app.log
config = PRConfig(config=server_app.config)
setup_handlers(server_app.web_app, config)
server_app.log.info("Registered jupyterlab_pullrequests extension")

setup_handlers(server_app.web_app, config, log=log)

log.info("Registered jupyterlab_pullrequests extension")


# for legacy launching with notebok (e.g. Binder)
_jupyter_server_extension_paths = _jupyter_server_extension_points
load_jupyter_server_extension = _load_jupyter_server_extension
22 changes: 20 additions & 2 deletions jupyterlab_pullrequests/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List, NamedTuple, Optional

from traitlets import Enum, Unicode, default
from traitlets import Enum, Unicode, default, Bool
from traitlets.config import Configurable


Expand Down Expand Up @@ -50,6 +50,24 @@ class PRConfig(Configurable):
help="Base URL of the versioning service REST API.",
)

anonymous = Bool(
config=True,
default=False,
help="Whether API request should be made without authorization headers",
)

repo = Unicode(
config=True,
allow_none=True,
help="An optional repo name to seed PR listing, used by github-anonymous"
)

owner = Unicode(
config=True,
allow_none=True,
help="An option user/organization to seed PR listing, used by github-anonymous"
)

@default("api_base_url")
def set_default_api_base_url(self):
if self.provider == "gitlab":
Expand All @@ -58,7 +76,7 @@ def set_default_api_base_url(self):
return "https://api.github.com"

provider = Enum(
["github", "gitlab"],
["github", "github-anonymous", "gitlab"],
default_value="github",
config=True,
help="The source control provider.",
Expand Down
40 changes: 24 additions & 16 deletions jupyterlab_pullrequests/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import logging
import traceback
from typing import Optional
from http import HTTPStatus

import tornado
Expand Down Expand Up @@ -198,26 +199,33 @@ def get_body_value(handler):
]


def setup_handlers(web_app: "NotebookWebApplication", config: PRConfig):
def setup_handlers(web_app: tornado.web.Application, config: PRConfig, log: Optional[logging.Logger]=None):
host_pattern = ".*$"
base_url = url_path_join(web_app.settings["base_url"], NAMESPACE)

logger = get_logger()
log = log or logging.getLogger(__name__)

manager_class = MANAGERS.get(config.provider)
if manager_class is None:
logger.error(f"No manager defined for provider '{config.provider}'.")
log.error(f"PR Manager: No manager defined for provider '{config.provider}'.")
raise NotImplementedError()
manager = manager_class(config.api_base_url, config.access_token)

web_app.add_handlers(
host_pattern,
[
(
url_path_join(base_url, pat),
handler,
{"logger": logger, "manager": manager},
)
for pat, handler in default_handlers
],
)
log.info(f"PR Manager Class {manager_class}")
try:
manager = manager_class(config)
except Exception as err:
import traceback
logging.error("PR Manager Exception", exc_info=1)
raise err

handlers = [
(
url_path_join(base_url, pat),
handler,
{"logger": log, "manager": manager},
)
for pat, handler in default_handlers
]

log.debug(f"PR Handlers: {handlers}")

web_app.add_handlers(host_pattern, handlers)
7 changes: 6 additions & 1 deletion jupyterlab_pullrequests/managers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
from .github_anonymous import AnonymousGithubManager
from .github import GitHubManager
from .gitlab import GitLabManager

# Supported third-party services
MANAGERS = {"github": GitHubManager, "gitlab": GitLabManager}
MANAGERS = {
"github-anonymous": AnonymousGithubManager,
"github": GitHubManager,
"gitlab": GitLabManager,
}
35 changes: 17 additions & 18 deletions jupyterlab_pullrequests/managers/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,20 @@
from tornado.httputil import url_concat
from tornado.web import HTTPError

from ..base import CommentReply, NewComment
from ..base import CommentReply, NewComment, PRConfig
from .manager import PullRequestsManager


class GitHubManager(PullRequestsManager):
"""Pull request manager for GitHub."""

def __init__(
self, base_api_url: str = "https://api.github.com", access_token: str = ""
) -> None:
"""
Args:
base_api_url: Base REST API url for the versioning service
access_token: Versioning service access token
"""
super().__init__(base_api_url=base_api_url, access_token=access_token)
self._pull_requests_cache = {} # Dict[str, Dict]
def __init__(self, config: PRConfig) -> None:
super().__init__(config)
self._pull_requests_cache = {}

@property
def base_api_url(self):
return self._config.api_base_url or "https://api.github.com"

@property
def per_page_argument(self) -> Optional[Tuple[str, int]]:
Expand All @@ -40,7 +37,7 @@ async def get_current_user(self) -> Dict[str, str]:
Returns:
JSON description of the user matching the access token
"""
git_url = url_path_join(self._base_api_url, "user")
git_url = url_path_join(self.base_api_url, "user")
data = await self._call_github(git_url, has_pagination=False)

return {"username": data["login"]}
Expand Down Expand Up @@ -186,7 +183,7 @@ async def list_prs(self, username: str, pr_filter: str) -> List[Dict[str, str]]:

# Use search API to find matching pull requests and return
git_url = url_path_join(
self._base_api_url, "/search/issues?q=+state:open+type:pr" + search_filter
self.base_api_url, "/search/issues?q=+state:open+type:pr" + search_filter
)

results = await self._call_github(git_url)
Expand All @@ -204,7 +201,7 @@ async def list_prs(self, username: str, pr_filter: str) -> List[Dict[str, str]]:
)

# Reset cache
self._pull_requests_cache = {}
self._pull_requests_cache = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you reset the cache to None and not an empty dictionary? The idea to set as an empty dictionary is to allow to use the get method line 298.


return data

Expand Down Expand Up @@ -251,6 +248,7 @@ async def _call_github(
params: Optional[Dict[str, str]] = None,
media_type: str = "application/vnd.github.v3+json",
has_pagination: bool = True,
**headers
) -> Union[dict, str]:
"""Call GitHub

Expand All @@ -271,10 +269,11 @@ async def _call_github(
List or Dict: Create from JSON response body if load_json is True
str: Raw response body if load_json is False
"""
headers = {
"Accept": media_type,
"Authorization": f"token {self._access_token}",
}
headers = headers or {}
headers.update({"Accept": media_type})

if not self.anonymous:
headers.update({"Authorization": f"token {self._config.access_token}"})

return await super()._call_provider(
url,
Expand Down
Loading