Skip to content

Commit 11585dc

Browse files
authored
Merge pull request #23 from DenisaCG/contentsManager
Set up backend content manager
2 parents ed558ba + ef93d9a commit 11585dc

File tree

12 files changed

+169
-311
lines changed

12 files changed

+169
-311
lines changed

conftest.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ def drives_base_config():
1717

1818

1919
@pytest.fixture
20-
def drives_s3_config(drives_base_config):
20+
def drives_config(drives_base_config):
2121
return drives_base_config()
2222

2323

2424
@pytest.fixture
25-
def drives_s3_manager(drives_base_config):
26-
from .jupyter_drives.managers.s3 import S3Manager
25+
def drives_manager(drives_base_config):
26+
from .jupyter_drives.manager import JupyterDrivesManager
2727

28-
return S3Manager(drives_base_config)
28+
return JupyterDrivesManager(drives_base_config)
2929

3030

3131
@pytest.fixture
32-
def drives_valid_s3_manager(drives_s3_manager):
33-
drives_s3_manager._config.access_key_id = "valid"
34-
drives_s3_manager._config.secret_access = "valid"
35-
return drives_s3_manager
32+
def drives_valid_manager(drives_manager):
33+
drives_manager._config.access_key_id = "valid"
34+
drives_manager._config.secret_access = "valid"
35+
return drives_manager

jupyter_drives/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@ def _load_jupyter_server_extension(server_app):
3333
JupyterLab application instance
3434
"""
3535
from .handlers import setup_handlers
36-
from .base import DrivesConfig
3736

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

4241
# Entry points
43-
def get_s3_manager(config: "traitlets.config.Config") -> "jupyter_drives.managers.JupyterDrivesManager":
44-
"""S3 Manager factory"""
45-
from .managers.s3 import S3Manager
42+
def get_manager(config: "traitlets.config.Config") -> "jupyter_drives.managers.JupyterDrivesManager":
43+
"""Drives Manager factory"""
44+
from .manager import JupyterDrivesManager
4645

47-
return S3Manager(config)
46+
return JupyterDrivesManager(config)

jupyter_drives/base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
# Supported third-party services
88
MANAGERS = {}
99

10+
# Moved to the architecture of having one provider independent manager.
11+
# Keeping the loop in case of future developments that need this feature.
1012
for entry in entrypoints.get_group_all("jupyter_drives.manager_v1"):
1113
MANAGERS[entry.name] = entry
1214

15+
# Supported providers
16+
PROVIDERS = ['s3', 'gcs', 'http']
17+
1318
class DrivesConfig(Configurable):
1419
"""
1520
Allows configuration of supported drives via jupyter_notebook_config.py
@@ -65,7 +70,7 @@ def set_default_api_base_url(self):
6570
return "https://www.googleapis.com/"
6671

6772
provider = Enum(
68-
MANAGERS.keys(),
73+
PROVIDERS,
6974
default_value="s3",
7075
config=True,
7176
help="The source control provider.",

jupyter_drives/handlers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import traitlets
1313

1414
from .base import MANAGERS, DrivesConfig
15-
from .managers.manager import JupyterDrivesManager
15+
from .manager import JupyterDrivesManager
1616

1717
NAMESPACE = "jupyter-drives"
1818

@@ -59,7 +59,7 @@ async def get(self):
5959
async def post(self):
6060
body = self.get_json_body()
6161
result = await self._manager.mount_drive(**body)
62-
self.finish(result["message"])
62+
self.finish(result)
6363

6464
class ContentsJupyterDrivesHandler(JupyterDrivesAPIHandler):
6565
"""
@@ -99,7 +99,7 @@ def setup_handlers(web_app: tornado.web.Application, config: traitlets.config.Co
9999
log = log or logging.getLogger(__name__)
100100

101101
provider = DrivesConfig(config=config).provider
102-
entry_point = MANAGERS.get(provider)
102+
entry_point = MANAGERS.get('drives_manager')
103103
if entry_point is None:
104104
log.error(f"JupyterDrives Manager: No manager defined for provider '{provider}'.")
105105
raise NotImplementedError()
Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import abc
21
import http
32
import json
43
import logging
54
from typing import Dict, List, Optional, Tuple, Union, Any
65

7-
import nbformat
86
import tornado
9-
import traitlets
107
import httpx
8+
import traitlets
119
from jupyter_server.utils import url_path_join
1210

13-
from ..log import get_logger
14-
from ..base import DrivesConfig
11+
import obstore as obs
12+
from libcloud.storage.types import Provider
13+
from libcloud.storage.providers import get_driver
14+
15+
from .log import get_logger
16+
from .base import DrivesConfig
1517

1618
import re
1719

18-
class JupyterDrivesManager(abc.ABC):
20+
class JupyterDrivesManager():
1921
"""
20-
Abstract base class for jupyter-drives manager.
22+
Jupyter-drives manager class.
2123
2224
Args:
2325
config: Server extension configuration object
@@ -26,12 +28,12 @@ class JupyterDrivesManager(abc.ABC):
2628
2729
The manager will receive the global server configuration object;
2830
so it can add configuration parameters if needed.
29-
It needs them to extract the ``DrivesConfig`` from it to pass it to this
30-
parent class (see ``S3Manager`` for an example).
31+
It needs them to extract the ``DrivesConfig``.
3132
"""
32-
def __init__(self, config: DrivesConfig) -> None:
33-
self._config = config
33+
def __init__(self, config: traitlets.config.Config) -> None:
34+
self._config = DrivesConfig(config=config)
3435
self._client = httpx.AsyncClient()
36+
self._content_managers = {}
3537

3638
@property
3739
def base_api_url(self) -> str:
@@ -50,19 +52,56 @@ def per_page_argument(self) -> Optional[Tuple[str, int]]:
5052
[str, int]: (query argument name, value)
5153
None: the provider does not support pagination
5254
"""
53-
return None
55+
return ("per_page", 100)
5456

55-
@abc.abstractclassmethod
5657
async def list_drives(self):
5758
"""Get list of available drives.
5859
5960
Returns:
6061
List of available drives and their properties.
6162
"""
62-
raise NotImplementedError()
63+
data = []
64+
if self._config.access_key_id and self._config.secret_access_key:
65+
if self._config.provider == "s3":
66+
S3Drive = get_driver(Provider.S3)
67+
drives = [S3Drive(self._config.access_key_id, self._config.secret_access_key)]
68+
69+
elif self._config.provider == 'gcs':
70+
GCSDrive = get_driver(Provider.GOOGLE_STORAGE)
71+
drives = [GCSDrive(self._config.access_key_id, self._config.secret_access_key)] # verfiy credentials needed
72+
73+
else:
74+
raise tornado.web.HTTPError(
75+
status_code= httpx.codes.NOT_IMPLEMENTED,
76+
reason="Listing drives not supported for given provider.",
77+
)
78+
79+
results = []
80+
for drive in drives:
81+
results += drive.list_containers()
82+
83+
for result in results:
84+
data.append(
85+
{
86+
"name": result.name,
87+
"region": self._config.region_name if self._config.region_name is not None else "eu-north-1",
88+
"creation_date": result.extra["creation_date"],
89+
"mounted": "true" if result.name not in self._content_managers else "false",
90+
"provider": self._config.provider
91+
}
92+
)
93+
else:
94+
raise tornado.web.HTTPError(
95+
status_code= httpx.codes.BAD_REQUEST,
96+
reason="No credentials specified. Please set them in your user jupyter_server_config file.",
97+
)
98+
99+
response = {
100+
"data": data
101+
}
102+
return response
63103

64-
@abc.abstractclassmethod
65-
async def mount_drive(self, drive_name, **kwargs):
104+
async def mount_drive(self, drive_name, provider, region):
66105
"""Mount a drive.
67106
68107
Args:
@@ -71,46 +110,75 @@ async def mount_drive(self, drive_name, **kwargs):
71110
Returns:
72111
The content manager for the drive.
73112
"""
74-
raise NotImplementedError()
113+
try:
114+
# check if content manager doesn't already exist
115+
if drive_name not in self._content_managers or self._content_managers[drive_name] is None:
116+
if provider == 's3':
117+
store = obs.store.S3Store.from_url("s3://" + drive_name + "/", config = {"aws_access_key_id": self._config.access_key_id, "aws_secret_access_key": self._config.secret_access_key, "aws_region": region})
118+
elif provider == 'gcs':
119+
store = obs.store.GCSStore.from_url("gs://" + drive_name + "/", config = {}) # add gcs config
120+
elif provider == 'http':
121+
store = obs.store.HTTPStore.from_url(drive_name, client_options = {}) # add http client config
122+
123+
self._content_managers[drive_name] = store
124+
125+
else:
126+
raise tornado.web.HTTPError(
127+
status_code= httpx.codes.CONFLICT,
128+
reason= "Drive already mounted."
129+
)
130+
131+
except Exception as e:
132+
raise tornado.web.HTTPError(
133+
status_code= httpx.codes.BAD_REQUEST,
134+
reason= f"The following error occured when mouting the drive: {e}"
135+
)
136+
137+
return
75138

76-
@abc.abstractclassmethod
77-
async def unmount_drive(self, drive_name: str, **kwargs):
139+
async def unmount_drive(self, drive_name: str):
78140
"""Unmount a drive.
79141
80142
Args:
81143
drive_name: name of drive to unmount
82144
"""
83-
raise NotImplementedError()
145+
if drive_name in self._content_managers:
146+
self._content_managers.pop(drive_name, None)
147+
148+
else:
149+
raise tornado.web.HTTPError(
150+
status_code= httpx.codes.NOT_FOUND,
151+
reason="Drive is not mounted or doesn't exist.",
152+
)
153+
154+
return
84155

85-
@abc.abstractclassmethod
86156
async def get_contents(self, drive_name, path, **kwargs):
87157
"""Get contents of a file or directory.
88158
89159
Args:
90160
drive_name: name of drive to get the contents of
91161
path: path to file or directory
92162
"""
93-
raise NotImplementedError()
163+
print('Get contents function called.')
94164

95-
@abc.abstractclassmethod
96165
async def new_file(self, drive_name, path, **kwargs):
97166
"""Create a new file or directory at the given path.
98167
99168
Args:
100169
drive_name: name of drive where the new content is created
101170
path: path where new content should be created
102171
"""
103-
raise NotImplementedError()
172+
print('New file function called.')
104173

105-
@abc.abstractclassmethod
106174
async def rename_file(self, drive_name, path, **kwargs):
107175
"""Rename a file.
108176
109177
Args:
110178
drive_name: name of drive where file is located
111179
path: path of file
112180
"""
113-
raise NotImplementedError()
181+
print('Rename file function called.')
114182

115183
async def _call_provider(
116184
self,

jupyter_drives/managers/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)