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

Re-architect backend tornado http server. Adds supporting decoupled library packages. (Large PR) #80

Merged
merged 133 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from 111 commits
Commits
Show all changes
133 commits
Select commit Hold shift + click to select a range
1529dcd
add pathlib helper utilities
Jul 8, 2021
02a54f5
unit test for pathlib utils
Jul 8, 2021
6a54967
remove erroneous shebang
Jul 8, 2021
b37b278
use relative imports instead of re-importing
Jul 8, 2021
3946c89
make utlities dir a python subpackage
Jul 8, 2021
928ee95
refactor ResourceManager constructor
Jul 8, 2021
b1ca13b
refactor authenticate and is_authenticated methods
Jul 8, 2021
b48b7eb
return user info on auth
Jul 8, 2021
8fff21c
refactor save_resource_locally
Jul 8, 2021
f01be36
remove unnecessary shebang and imports
Jul 9, 2021
3ae112d
add httpstatus import and use relative local imports
Jul 9, 2021
ea191a0
minor formatting and add TODO: move constants to config file
Jul 9, 2021
af2641d
temporarily change baseclass of BaseRequestHandler for dev work
Jul 9, 2021
863cc21
override base class prepare to return 401 unauthorized if not authent…
Jul 9, 2021
7db757a
use httpstatus enum to set options status in base class
Jul 9, 2021
729a2c5
HeaderMixIn class that adds headers in _custom_headers instance variable
Jul 9, 2021
d98123f
Child Handlers now impliment HeaderMixIn. Added TODOs and Notes
Jul 9, 2021
1c90a62
remove redundant is_authenticated logic from child handlers
Jul 9, 2021
cdd4701
add pydantic as setup requirement
Jul 14, 2021
c07c6dc
start transition from hs_restclient to hsclient. Add sessionstruct an…
Jul 14, 2021
4bea599
pydantic api models
Jul 14, 2021
f57560a
add session struct container for holding session and session cookie
Jul 14, 2021
eec61c8
add session struct unit tests
Jul 14, 2021
b1f93a4
remove remember field from auth api model
Jul 15, 2021
de3192f
session mixin that extends functionality of IPythonHandler
Jul 15, 2021
8695522
add prepare to base handler. Requires user auth or redirects. Also ad…
Jul 15, 2021
088fb03
mixin for mutating global session object. Allows for speration from c…
Jul 15, 2021
7318e0b
LoginHandler updated. Credentials no longer stored. A salted secure c…
Jul 15, 2021
df18db6
update LoginHandler docstring
Jul 15, 2021
eb6d07f
replace previous server unit tests. More to come
Jul 15, 2021
916db40
add pytest-tornado to development requirements
Jul 15, 2021
4912873
pytest config file. By default print logs. Disable using 'pytest -o l…
Jul 15, 2021
508d846
move server.py if name == main functionality to module level __main__…
Jul 15, 2021
bef4f9a
add id field to SessionStruct
Jul 15, 2021
0a47ae5
update login logic to store user id
Jul 15, 2021
20af2fc
fix session struct type hints
Jul 15, 2021
e097b48
UserInfoHandler now uses hsclient. Remove TestApp from server.py
Jul 15, 2021
f5c8902
remove if name == main from server.py
Jul 15, 2021
57523fc
add CLI to __main__. Can pass port, hostname, --no-debug, and config …
Jul 16, 2021
b7a08b2
add username field and implimentation to session struct
Jul 16, 2021
309ac0a
add note regarding Hsmd5Handler. Not sure that Hsmd5Handler is implim…
Jul 16, 2021
28f2765
add and implement pydantic schema CollectionOfResourceMetadata. a lis…
Jul 16, 2021
4a6d1e5
add tests for CollectionOfResourceMetadata
Jul 16, 2021
1e82547
add a few comments and todos to server endpoints
Jul 16, 2021
8099b18
pathlib utilities for verifying if a path is a descendant of some parent
Jul 16, 2021
ab41bdf
add pathlib utils tests
Jul 16, 2021
a8fad0d
Enum and schema for creating new hydroshare resource via rest
Jul 16, 2021
6fcdddb
add configuration file parser that uses pydantic.
Jul 19, 2021
51cef05
unit tests for configuration parser
Jul 19, 2021
701a440
first_existing_file func added to utilites. Give a list of paths and …
Jul 19, 2021
81fc01c
add pydantic[dotenv] as dep. Replaces pydantic. Needed for config parser
Jul 19, 2021
7c42a40
update help message in --config cli argument
Jul 19, 2021
56cbda6
move argparser to cli module. move parser to arg for main for dep inj…
Jul 19, 2021
7ccfd72
Rename ResourcesRootHandler to ListUserHydroShareResources. Comment o…
Jul 19, 2021
94b6d30
update ResourcesRootHandler to ListUserHydroShareResources handler in…
Jul 19, 2021
acf85de
impliment configuration parser in main
Jul 21, 2021
fa7f105
expand and resolve data and log paths in config
Jul 21, 2021
2db1406
add ResourceFiles pydantic api model. This is mainly for marshalling …
Jul 21, 2021
8f9b4bd
Extend hsclient::HydroShare with HydroShareWithResourceCache. As the …
Jul 21, 2021
ec9ec89
add/replace rest api handlers for listing and downloading hydroshare …
Jul 21, 2021
475e4a9
add hs resource strategy class tree and strategy factory
Jul 21, 2021
e27f737
update gitignore to omit lib dir outside of root context
Jul 21, 2021
8634426
add boolean api model
Jul 21, 2021
e5c30b6
update and rename resource handler to impliment resource factory. Now…
Jul 21, 2021
c523894
update class name in HydroShareResourceEntityHandler static method
Jul 21, 2021
fc38171
add 'data_path' property to base handler.
Jul 21, 2021
71cdf4b
ResourceCreationRequest now uses regex to verify that path names do n…
Jul 23, 2021
5418e9c
add data_path property to base handler
Jul 23, 2021
5535343
add LocalResourceEntityHandler for uploading local HS resource data t…
Jul 23, 2021
3fbbb90
unit test regex implimentation in ResourceFiles
Jul 23, 2021
2bd2a9b
require 'Content-Type: application/json' in login requests
Jul 23, 2021
1662804
remove erroneous test_hello_world unit test
Jul 23, 2021
80a453b
add root.html template
Jul 29, 2021
c369bf1
add template path to development Application object
Jul 29, 2021
0e0ce29
render root.html in catch all / handler. WIP: prepare method will nee…
Jul 29, 2021
b895b5d
login route requires json content-type header. access control allow o…
Jul 29, 2021
2d6f776
login cookie is now a session cookie instead of a 30 day persistant c…
Jul 29, 2021
67330d7
remove erroneous import in __main__
Jul 29, 2021
29a727f
add date_created and date_last_updated fields to ResourceMetadata api…
Jul 29, 2021
6ac52a1
update tests to support changes. Namely, /syncApi/login content-type …
Jul 29, 2021
0b6cc5e
rework root __init__.py. Now supports lab and server extension.
Aug 5, 2021
a5df1fe
load config data from string not file object
Aug 5, 2021
3f997f8
include labextension dir in manifest
Aug 13, 2021
3e0d680
render static html from template; plus type hints
Aug 13, 2021
68c673e
add file system resource types
Aug 13, 2021
220d380
fs resource map irepresenting the relationship between a local file (…
Aug 13, 2021
7411dc0
add __init__.py to make filesystem lib a module
Aug 13, 2021
682f923
FS map interfaces and concrete implementations. These objects represe…
Aug 13, 2021
3d77d1c
AggregateFSMap conrete implementation. Encapsulates a local and remot…
Aug 13, 2021
14ed760
AggregateFSResourceMapSyncState is a pydantic model that describes th…
Aug 13, 2021
e6db8f6
filesystem lib specific expections
Aug 13, 2021
e60c295
simple event library. subscribe, unsubscribe, and dispatch events. Ev…
Aug 13, 2021
e8dae7e
application specific event enum
Aug 13, 2021
88a94b5
application specific object interface describing objects necessary to…
Aug 13, 2021
877bf71
application specific event listeners implimentations
Aug 13, 2021
53bac38
SessionSyncStruct. conglomerate object that implements the setup and …
Aug 13, 2021
e910ccc
application specific watchdog event handler factory. Creates file sys…
Aug 13, 2021
7cdf7c5
add FileSystemEventWebSocketHandler tornado websocket handler. the sy…
Aug 13, 2021
60b942d
add _SessionSyncSingleton. A thin wrapper for managing a SessionSyncS…
Aug 13, 2021
9a6e1dc
add FileSystemEventWebSocketHandler to tornado application handlers
Aug 13, 2021
1300c1a
add logic to tornado handlers that download and upload hydroshare res…
Aug 13, 2021
c67554f
update server unit tests
Aug 13, 2021
d254562
add unit tests for AggregateFSMap and FSResourceMaps
Aug 13, 2021
644203f
add handlers msubpackage This is a WIP. Handlers previously found in …
Aug 17, 2021
0e3f79c
comment out websocket handler in server.py. Causes circular import. W…
Aug 17, 2021
f7a4249
__main__.py and __init__.py now import get_route_handlers from the ha…
Aug 17, 2021
6a5d701
use data_path and log_path as config file deserialized names. These s…
Aug 18, 2021
7e47e33
update config file unit tests
Aug 18, 2021
3d9cf33
no longer manually set application config settings in TestApp. instea…
Aug 18, 2021
f35e21e
jupyter server extension now utilizes configuration settings.
Aug 18, 2021
9d70310
add deprecated comment to handlers no longer used. Aid in future refa…
Aug 18, 2021
871cf69
update fs_event_handler_factory docstring to note the one-to-one rela…
Aug 23, 2021
2db44ca
change FSEventHanlder logging from INFO to DEBUG level
Aug 23, 2021
760b954
add watchdog dep to setup.py
Aug 25, 2021
d2fc2bd
use user can edit criteria instead of user is owner critera. See http…
Aug 25, 2021
248e91f
rename 'data_dir' to more representative 'contents_dir' per https://g…
Aug 25, 2021
14b0e71
update references from 'data_dir' to 'contents_dir' in application sp…
Aug 25, 2021
65a4162
fix undefined reference to session sync object in post login handlers
Aug 25, 2021
9aa1d61
remove extraneous __init__ return types from lib filesystem per https…
Aug 25, 2021
004765b
remove extraneous imports from lib filesystem
Aug 25, 2021
14a1493
update jupyter lab plugin entry point function name. _jupyter_server_…
Sep 27, 2021
dd700ba
enum, fs_events.Events, uses auto for enumeration instead of duplicat…
Sep 27, 2021
85d0d3c
fix bug in resource_strategies. intermediary directories now create t…
Sep 27, 2021
df811fc
add authors and creator field to ResourceMetadata pydantic model. Cov…
Sep 27, 2021
0245c4e
add DataDir pydantic model. Main use if for conveying the configured …
Sep 27, 2021
a4b76a7
resolve bug in tornado entity handlers. Path to resources files impro…
Sep 27, 2021
a806bf7
add DataDirectoryHandler which returns the configured hydroshare jupy…
Sep 27, 2021
91e43ed
add DataDirectoryHandler to handlers registery
Sep 27, 2021
df64657
SessionSyncEventListeners.resource_entity_downloaded bug fix (include…
Sep 27, 2021
2bfffae
add missing event type subscriptions to websocket handler. also, adds…
Sep 27, 2021
78ebaba
include all files under hydroshare_jupyter_sync/labextension dir
Sep 27, 2021
6b20457
add LocalResourceEntityHandler unit tests
Sep 27, 2021
d5e0df2
update fs resource map unit tests to create the corrent intermediate …
Sep 27, 2021
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ develop-eggs/
downloads/
eggs/
.eggs/
lib/
/lib/
lib64/
parts/
sdist/
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
include hydroshare_jupyter_sync/assets/*
include hydroshare_jupyter_sync/labextension
99 changes: 55 additions & 44 deletions hydroshare_jupyter_sync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,58 @@
'''
This file sets up the jupyter server extension to launch our backend
server when jupyter is launched.

Author: 2019-20 CUAHSI Olin SCOPE Team
Vicky McDermott, Kyle Combes, Emily Lepert, and Charlie Weiss
'''
# !/usr/bin/python
# -*- coding: utf-8

import logging
from hydroshare_jupyter_sync.config_reader_writer import get_config_values
from hydroshare_jupyter_sync.index_html import (set_backend_url,
set_frontend_url)
from .server import get_route_handlers
from notebook.utils import url_path_join


def _jupyter_server_extension_paths():
"""Creates the path to load the jupyter server extension.
"""
return [{
"module": "hydroshare_jupyter_sync"
}]
"""
Setup jupyterlab server and lab extensions. Add tornado HTTP handlers to jupyter session.
"""
import json
from pathlib import Path

# local imports
from .config_setup import ConfigFile
from .handlers import get_route_handlers

# Constants
FRONTEND_PATH = "/sync"
BACKEND_PATH = "/syncApi"

MODULE_NAME = "hydroshare_jupyter_sync"
EXTENSION_DIRNAME = "labextension"

PARENT_DIR = Path(__file__).parent.resolve()
EXTENSION_METADATA_PATH = PARENT_DIR / f"{EXTENSION_DIRNAME}/package.json"

# read metadata from js extension package metadata file, `package.json`
extension_metadata = json.loads(EXTENSION_METADATA_PATH.read_text())


def _jupyter_labextension_paths():
return [{"src": EXTENSION_DIRNAME, "dest": extension_metadata["name"]}]


def load_jupyter_server_extension(nb_server_app):
"""Sets up logging to a specific file, sets frontend & backend urls,
and loads up the server extension.
def _jupyter_server_extension_points():
return [{"module": MODULE_NAME}]


def _load_jupyter_server_extension(server_app):
"""Registers the API handler to receive HTTP requests from the frontend extension.

Parameters
----------
server_app: jupyterlab.labapp.LabApp
JupyterLab application instance
"""
nb_server_app.log.info("Successfully loaded hydroshare_jupyter_sync server "
"extension.")

config = get_config_values(['logPath'])
log_file_path = None
if config:
log_file_path = config.get('logPath')
logging.basicConfig(level=logging.DEBUG, filename=log_file_path)

web_app = nb_server_app.web_app

frontend_base_url = url_path_join(web_app.settings['base_url'], 'sync')
backend_base_url = url_path_join(web_app.settings['base_url'], 'syncApi')
set_backend_url(backend_base_url)
set_frontend_url(frontend_base_url)
handlers = get_route_handlers(frontend_base_url, backend_base_url)
web_app.add_handlers('.*$', handlers)
handlers = get_route_handlers(FRONTEND_PATH, BACKEND_PATH)

# `cookie_secret` inherited from `server_app`
server_app.web_app.add_handlers(".*$", handlers)
server_app.log.info(f"Registered {MODULE_NAME} extension")

# parse config file. if env variables present, they take precedence.
# looks for config in following order:
# 1. "~/.config/hydroshare_jupyter_sync/config"
# 2. "~/.hydroshare_jupyter_sync_config"
config = ConfigFile()

# pass config file settings to Tornado Application (web app)
server_app.web_app.settings.update(config.dict())


# For backward compatibility with the classical notebook
load_jupyter_server_extension = _load_jupyter_server_extension
72 changes: 72 additions & 0 deletions hydroshare_jupyter_sync/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import argparse
import logging
import signal
import sys

from tornado import options
from tornado import ioloop
from tornado.web import Application
from pathlib import Path
from .handlers import get_route_handlers
from .cli import parse
from .config_setup import ConfigFile


class TestApp(Application):
"""Class for setting up the server & making sure it can exit cleanly"""

is_closing = False

def signal_handler(self, signum, frame):
logging.info("Shutting down")
self.is_closing = True

def try_exit(self):
if self.is_closing:
ioloop.IOLoop.instance().stop()
logging.info("Exit successful")


def get_test_app(**settings) -> Application:
"""Thin wrapper returning web.Application instance. Written in this way for use in
unit tests.
"""
return TestApp(
get_route_handlers("/", "/syncApi"),
cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
login_url="/syncApi/login",
template_path=Path(__file__).resolve().parent / "templates",
**settings,
)


def main(parser: argparse.Namespace):
# clear argv. options parse command line somehow sets up logging
# tornados logging setup is pretty broken. Do not want to pass any command line args
# here
sys.argv = []
options.parse_command_line()
debug_enabled = not parser.no_debug
# TODO: write logs to file in config.log

# parse config file
config = (
ConfigFile() if parser.config is None else ConfigFile(_env_file=parser.config)
)

app = get_test_app(
default_hostname=parser.hostname, debug=debug_enabled, **config.dict()
)

logging.info(f"Server starting on {parser.hostname}:{parser.port}")
logging.info(f"Debugging mode {'enabled' if debug_enabled else 'disabled'}")

signal.signal(signal.SIGINT, app.signal_handler)
app.listen(parser.port)
ioloop.PeriodicCallback(app.try_exit, 100).start()
ioloop.IOLoop.instance().start()


if __name__ == "__main__":
parser = parse()
main(parser)
73 changes: 73 additions & 0 deletions hydroshare_jupyter_sync/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import argparse
from pathlib import Path
from typing import Union
from .utilities.pathlib_utils import expand_and_resolve


def parse() -> Union[argparse.Namespace, None]:
parser = argparse.ArgumentParser(
prog="hydroshare_jupyter_sync",
description=(
"""HydroShare Jupyter Sync:\n\t
A Jupyter server extension enabling management of HydroShare resource
files within Jupyter. Open HydroShare resources, work on their files,
and then sync those changes back to HydroShare using a drag-and-drop
interface.

Note:
Debug mode is enabled by default when starting the server via this CLI.
"""
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter, # This adds defaults to help page
)

parser.add_argument(
"-p",
"--port",
type=int,
nargs="?",
help="Port number to listen on",
default=8080,
)

parser.add_argument(
"-n",
"--hostname",
type=str,
nargs="?",
help="HTTP Server hostname",
default="127.0.0.1", # localhost
)

parser.add_argument(
"-d",
"--no-debug",
action="store_true",
default=False,
help="Disable debugging mode",
)

parser.add_argument(
"-c",
"--config",
nargs="?",
type=absolute_file_path,
help="Path to configuration file. By default read from ~/.config/hydroshare_jupyter_sync/config then ~/.hydroshare_jupyter_sync_config if either exist.",
required=False,
)

return parser.parse_args()


def is_file_and_exists(f: Union[str, Path]) -> bool:
"""Expand and resolve path and return if it is a file."""
f = expand_and_resolve(f)
return f.is_file() and f.exists()


def absolute_file_path(f: Union[str, Path]) -> str:
"""Return absolute path to file, if exists."""
f = expand_and_resolve(f)
if is_file_and_exists(f):
return str(f)
raise FileNotFoundError(f"File: {f}, does not exist.")
40 changes: 40 additions & 0 deletions hydroshare_jupyter_sync/config_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from pydantic import BaseSettings, Field, root_validator
from pathlib import Path
from typing import Union
from .utilities.pathlib_utils import first_existing_file, expand_and_resolve

_DEFAULT_CONFIG_FILE_LOCATIONS = (
"~/.config/hydroshare_jupyter_sync/config",
"~/.hydroshare_jupyter_sync_config",
)

_DEFAULT_DATA_PATH = expand_and_resolve("~/hydroshare")
_DEFAULT_LOG_PATH = expand_and_resolve("~/hydroshare/logs")


class FileNotDirectoryError(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)


class ConfigFile(BaseSettings):
# case-insensitive alias values DATA and LOG
data_path: Path = Field(_DEFAULT_DATA_PATH, env="data")
log_path: Path = Field(_DEFAULT_LOG_PATH, env="log")

class Config:
env_file: Union[str, None] = first_existing_file(_DEFAULT_CONFIG_FILE_LOCATIONS)
env_file_encoding = "utf-8"

@root_validator
def create_paths_if_do_not_exist(cls, values: dict):
for key, path in values.items():
path = expand_and_resolve(path)
if path.is_file():
raise FileNotDirectoryError(
f"Configuration setting: {key}={str(path)} is a file not a directory."
)
elif not path.exists():
path.mkdir()

return values
92 changes: 92 additions & 0 deletions hydroshare_jupyter_sync/fs_event_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from watchdog.events import (
FileSystemEventHandler,
PatternMatchingEventHandler,
FileCreatedEvent,
FileModifiedEvent,
FileDeletedEvent,
FileMovedEvent,
FileClosedEvent,
)

from .fs_events import Events
from .lib.filesystem.fs_resource_map import LocalFSResourceMap
from .lib.events.event_broker import EventBroker

from functools import wraps
import logging

# module level log
logger = logging.getLogger(__name__)


def log_event(fn):
@wraps(fn)
def wrapper(self, event):
logger.info(event)
aaraney marked this conversation as resolved.
Show resolved Hide resolved
return fn(self, event)

return wrapper


def fs_event_handler_factory(event_broker: EventBroker) -> FileSystemEventHandler:
"""Wrap FSEventHandler in event_broker context."""

class FSEventHandler(PatternMatchingEventHandler):
def __init__(self, local_fs_map: LocalFSResourceMap):
# TODO: use pattern kwarg to ignore certain files/file extensions. it would be nice if
# this were a configurable.
super().__init__(ignore_directories=True)

# dependency inject local filesystem map
self._res_map = local_fs_map

@log_event
def on_any_event(self, event):
# log all events
...

def on_created(self, event: FileCreatedEvent) -> None:
# add file to local fs map
self._res_map.add_file(event.src_path)

# dispatch new state
event_broker.dispatch(Events.STATUS, self.resource_id)
sblack-usu marked this conversation as resolved.
Show resolved Hide resolved

def on_modified(self, event: FileModifiedEvent) -> None:
# update file in local fs map
self._res_map.update_file(event.src_path)

# dispatch new state
event_broker.dispatch(Events.STATUS, self.resource_id)

def on_deleted(self, event: FileDeletedEvent) -> None:
# remove file from local fs map
self._res_map.delete_file(event.src_path)

# dispatch new state
event_broker.dispatch(Events.STATUS, self.resource_id)

def on_moved(self, event: FileMovedEvent) -> None:
# update/add file in local fs map, remove file from local fs map
self._res_map.delete_file(event.src_path)

# NOTE: change in the future. Right now, this covers all cases.
self._res_map.add_file(event.dest_path)
self._res_map.update_file(event.dest_path)
sblack-usu marked this conversation as resolved.
Show resolved Hide resolved

# dispatch new state
event_broker.dispatch(Events.STATUS, self.resource_id)

def on_closed(self, event: FileClosedEvent) -> None:
# update file in local fs map
self._res_map.update_file(event.src_path)

# dispatch new state
event_broker.dispatch(Events.STATUS, self.resource_id)

# properties
@property
def resource_id(self) -> str:
return self._res_map.resource_id
aaraney marked this conversation as resolved.
Show resolved Hide resolved

return FSEventHandler
Loading