Skip to content

Commit

Permalink
Merge pull request #3 from DenisaCG/listAvailableDrives
Browse files Browse the repository at this point in the history
Set up backend structure and list all available drives
  • Loading branch information
DenisaCG authored Nov 20, 2023
2 parents 45dc4af + 87b5e79 commit 878c178
Show file tree
Hide file tree
Showing 13 changed files with 655 additions and 38 deletions.
31 changes: 29 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
import pytest
from traitlets.config import Config

pytest_plugins = ("pytest_jupyter.jupyter_server", )


@pytest.fixture
def jp_server_config(jp_server_config):
return {"ServerApp": {"jpserver_extensions": {"jupyter_drives": True}}}
return {
"ServerApp": {"jpserver_extensions": {"jupyter_drives": True}},
"DrivesConfig": {"api_base_url": "https://s3.eu-north-1.amazonaws.com/", "access_key_id": "valid", "secret_access_key":"valid"},
}


@pytest.fixture
def drives_base_config():
return Config()


@pytest.fixture
def drives_s3_config(drives_base_config):
return drives_base_config()


@pytest.fixture
def drives_s3_manager(drives_base_config):
from .jupyter_drives.managers.s3 import S3Manager

return S3Manager(drives_base_config)


@pytest.fixture
def drives_valid_s3_manager(drives_s3_manager):
drives_s3_manager._config.access_key_id = "valid"
drives_s3_manager._config.secret_access = "valid"
return drives_s3_manager
15 changes: 13 additions & 2 deletions jupyter_drives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import warnings
warnings.warn("Importing 'jupyter_drives' outside a proper installation.")
__version__ = "dev"
from .handlers import setup_handlers

import traitlets


def _jupyter_labextension_paths():
Expand All @@ -31,6 +32,16 @@ def _load_jupyter_server_extension(server_app):
server_app: jupyterlab.labapp.LabApp
JupyterLab application instance
"""
setup_handlers(server_app.web_app)
from .handlers import setup_handlers
from .base import DrivesConfig

setup_handlers(server_app.web_app, server_app.config)
name = "jupyter_drives"
server_app.log.info(f"Registered {name} server extension")

# Entry points
def get_s3_manager(config: "traitlets.config.Config") -> "jupyter_drives.managers.JupyterDrivesManager":
"""S3 Manager factory"""
from .managers.s3 import S3Manager

return S3Manager(config)
63 changes: 63 additions & 0 deletions jupyter_drives/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import entrypoints
from traitlets import Enum, Unicode, default
from traitlets.config import Configurable

# Supported third-party services
MANAGERS = {}

for entry in entrypoints.get_group_all("jupyter_drives.manager_v1"):
MANAGERS[entry.name] = entry

class DrivesConfig(Configurable):
"""
Allows configuration of supported drives via jupyter_notebook_config.py
"""

session_token = Unicode(
None,
config=True,
allow_none=True,
help="A session access token to authenticate.",
)

access_key_id = Unicode(
None,
config=True,
allow_none=True,
help="The id of the access key for the bucket.",
)

secret_access_key= Unicode(
None,
config=True,
allow_none=True,
help="The secret access key for the bucket.",
)

region_name = Unicode(
"eu-north-1",
config = True,
help = "Region name.",
)

api_base_url = Unicode(
config=True,
help="Base URL of the provider service REST API.",
)

@default("api_base_url")
def set_default_api_base_url(self):
# for AWS S3 drives
if self.provider == "s3":
return "https://s3.amazonaws.com/" # region? https://s3.<region>.amazonaws.com/

# for Google Cloud Storage drives
elif self.provider == "gcs":
return "https://www.googleapis.com/"

provider = Enum(
MANAGERS.keys(),
default_value="s3",
config=True,
help="The source control provider.",
)
91 changes: 79 additions & 12 deletions jupyter_drives/handlers.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,91 @@
"""
Module with all of the individual handlers, which will return the results to the frontend.
"""
import json
import logging
import traceback
from typing import Optional

from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join
import tornado
import traitlets

class RouteHandler(APIHandler):
# The following decorator should be present on all verb methods (head, get, post,
# patch, put, delete, options) to ensure only authorized user can request the
# Jupyter server
from .base import MANAGERS, DrivesConfig
from .managers.manager import JupyterDrivesManager

NAMESPACE = "jupyter-drives"

class JupyterDrivesAPIHandler(APIHandler):
"""
Base handler for jupyter-drives specific API handlers
"""
def initialize(self, logger: logging.Logger, manager: JupyterDrivesManager):
self._jp_log = logger
self._manager = manager

def write_error(self, status_code, **kwargs):
"""
Override Tornado's RequestHandler.write_error for customized error handlings
This method will be called when an exception is raised from a handler
"""
self.set_header("Content-Type", "application/json")
reply = {"error": "Unhandled error"}
exc_info = kwargs.get("exc_info")
if exc_info:
e = exc_info[1]
if isinstance(e, tornado.web.HTTPError):
reply["error"] = e.reason
if hasattr(e, "error_code"):
reply["error_code"] = e.error_code
else:
reply["error"] = "".join(traceback.format_exception(*exc_info))
self.finish(json.dumps(reply))

class ListJupyterDrives(JupyterDrivesAPIHandler):
"""
Returns list of available drives.
"""
# Later on, filters can be added for the listing
@tornado.web.authenticated
def get(self):
self.finish(json.dumps({
"data": "This is /jupyter-drives/get-example endpoint!"
}))
async def get(self):
drives, error = await self._manager.list_drives()
self.finish(json.dumps(drives))
get_logger().debug(error)

default_handlers = [
("drives", ListJupyterDrives)
]

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

log = log or logging.getLogger(__name__)

provider = DrivesConfig(config=config).provider
entry_point = MANAGERS.get(provider)
if entry_point is None:
log.error(f"JupyterDrives Manager: No manager defined for provider '{provider}'.")
raise NotImplementedError()
manager_factory = entry_point.load()
log.info(f"JupyterDrives Manager Class {manager_factory}")
try:
manager = manager_factory(config)
except Exception as err:
import traceback
logging.error("JupyterDrives Manager Exception", exc_info=1)
raise err

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

log.debug(f"Jupyter-Drives Handlers: {handlers}")

base_url = web_app.settings["base_url"]
route_pattern = url_path_join(base_url, "jupyter-drives", "get-example")
handlers = [(route_pattern, RouteHandler)]
web_app.add_handlers(host_pattern, handlers)
19 changes: 19 additions & 0 deletions jupyter_drives/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import logging

from traitlets.config import Application

class _ExtensionLogger:
_LOGGER = None #type: Optional[logging.Logger]

@classmethod
def get_logger(cls) -> logging.Logger:
if cls._LOGGER is None:
app = Application.instance()
cls._LOGGER = logging.getLogger(
"{!s}.jupyter_drives".format(app.log.name)
)
Application.clear_instance()

return cls._LOGGER

get_logger = _ExtensionLogger.get_logger
Empty file.
Loading

0 comments on commit 878c178

Please sign in to comment.