From 206da759ad4ebd5f564a0ec04fa294fc7e5f15d3 Mon Sep 17 00:00:00 2001 From: pkdash Date: Wed, 9 Oct 2024 16:59:55 -0400 Subject: [PATCH] [#102] initial upgrade to pydantic v2 --- hydroshare_on_jupyter/__init__.py | 2 +- hydroshare_on_jupyter/__main__.py | 2 +- hydroshare_on_jupyter/config_setup.py | 52 ++++++----- hydroshare_on_jupyter/handlers/__init__.py | 4 +- .../aggregate_fs_resource_map_sync_state.py | 8 +- hydroshare_on_jupyter/models/api_models.py | 90 +++++++++++-------- hydroshare_on_jupyter/models/oauth.py | 64 +++++++------ hydroshare_on_jupyter/server.py | 28 +++--- setup.py | 5 +- tests/test_aggregate_fs_map.py | 17 ++-- tests/test_api_models.py | 14 +-- tests/test_config.py | 11 +-- tests/test_oauth_json_model.py | 4 +- tests/test_pathlib_utils.py | 10 ++- tests/test_server.py | 13 +-- 15 files changed, 192 insertions(+), 132 deletions(-) diff --git a/hydroshare_on_jupyter/__init__.py b/hydroshare_on_jupyter/__init__.py index f3def18c..33bb5d65 100644 --- a/hydroshare_on_jupyter/__init__.py +++ b/hydroshare_on_jupyter/__init__.py @@ -65,7 +65,7 @@ def _load_jupyter_server_extension(server_app: ServerApp): config = ConfigFile() # pass config file settings to Tornado Application (web app) - server_app.web_app.settings.update(config.dict()) + server_app.web_app.settings.update(config.model_dump()) # For backward compatibility with the classical notebook diff --git a/hydroshare_on_jupyter/__main__.py b/hydroshare_on_jupyter/__main__.py index 9807a18d..a31e4e75 100644 --- a/hydroshare_on_jupyter/__main__.py +++ b/hydroshare_on_jupyter/__main__.py @@ -114,7 +114,7 @@ def configure_jupyter() -> None: def start_stand_alone_session( hostname: str, port: int, debug: bool, config: ConfigFile ) -> None: - app = get_test_app(default_hostname=hostname, debug=debug, **config.dict()) + app = get_test_app(default_hostname=hostname, debug=debug, **config.model_dump()) logging.info(f"Server starting on {hostname}:{port}") logging.info(f"Debugging mode {'enabled' if debug else 'disabled'}") diff --git a/hydroshare_on_jupyter/config_setup.py b/hydroshare_on_jupyter/config_setup.py index 3811b9d2..6f99903c 100644 --- a/hydroshare_on_jupyter/config_setup.py +++ b/hydroshare_on_jupyter/config_setup.py @@ -1,4 +1,5 @@ -from pydantic import BaseSettings, Field, root_validator, validator +from pydantic import Field, field_validator, model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict import pickle from pathlib import Path from typing import Optional, Union @@ -21,27 +22,36 @@ def __init__(self, message: str) -> None: 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") - oauth_path: Union[OAuthFile, str, None] = Field(None, env="oauth") + data_path: Optional[Path] = Field(_DEFAULT_DATA_PATH, validation_alias="data") + log_path: Optional[Path] = Field(_DEFAULT_LOG_PATH, validation_alias="log") + oauth_path: Union[OAuthFile, str, None] = Field(None, validation_alias="oauth") - class Config: - env_file: Union[str, None] = first_existing_file(_DEFAULT_CONFIG_FILE_LOCATIONS) - env_file_encoding = "utf-8" + model_config = SettingsConfigDict( + env_file=first_existing_file(_DEFAULT_CONFIG_FILE_LOCATIONS), + env_file_encoding='utf-8' + ) + # TODO: cleanup + # class Config: + # env_file: Union[str, None] = first_existing_file(_DEFAULT_CONFIG_FILE_LOCATIONS) + # env_file_encoding = "utf-8" - @validator("data_path", "log_path", pre=True) - def create_paths_if_do_not_exist(cls, v: Path): - # for key, path in values.items(): - path = expand_and_resolve(v) - if path.is_file(): - raise FileNotDirectoryError( - f"Configuration path: {str(path)} is a file not a directory." - ) - elif not path.exists(): - path.mkdir(parents=True) - return path - - @validator("oauth_path") + @model_validator(mode="after") + def create_paths_if_do_not_exist(self): + + def check_path(path: Path): + path = expand_and_resolve(path) + if path.is_file(): + raise FileNotDirectoryError( + f"Configuration path: {str(path)} is a file not a directory." + ) + elif not path.exists(): + path.mkdir(parents=True) + + check_path(self.data_path) + check_path(self.log_path) + return self + + @field_validator("oauth_path") def unpickle_oauth_path(cls, v): if v is None: return v @@ -53,4 +63,4 @@ def unpickle_oauth_path(cls, v): with open(path, "rb") as f: deserialized_model = pickle.load(f) - return OAuthFile.parse_obj(deserialized_model) + return OAuthFile.model_validate(deserialized_model) diff --git a/hydroshare_on_jupyter/handlers/__init__.py b/hydroshare_on_jupyter/handlers/__init__.py index 0e38bc76..ea63fcd2 100644 --- a/hydroshare_on_jupyter/handlers/__init__.py +++ b/hydroshare_on_jupyter/handlers/__init__.py @@ -1,5 +1,7 @@ from pathlib import Path -from notebook.utils import url_path_join +# TODO: cleanup +# from notebook.utils import url_path_join +from jupyter_server.utils import url_path_join import tornado from ..websocket_handler import FileSystemEventWebSocketHandler from ..server import ( diff --git a/hydroshare_on_jupyter/lib/filesystem/aggregate_fs_resource_map_sync_state.py b/hydroshare_on_jupyter/lib/filesystem/aggregate_fs_resource_map_sync_state.py index a5a9dfc7..6d359050 100644 --- a/hydroshare_on_jupyter/lib/filesystem/aggregate_fs_resource_map_sync_state.py +++ b/hydroshare_on_jupyter/lib/filesystem/aggregate_fs_resource_map_sync_state.py @@ -1,5 +1,5 @@ from __future__ import annotations -from pydantic import BaseModel +from pydantic import BaseModel, RootModel from pathlib import Path from typing import Set, List, TYPE_CHECKING from .fs_resource_map import LocalFSResourceMap, RemoteFSResourceMap @@ -62,8 +62,8 @@ def from_resource_maps( ) -class AggregateFSResourceMapSyncStateCollection(BaseModel): - __root__: List[AggregateFSResourceMapSyncState] +class AggregateFSResourceMapSyncStateCollection(RootModel): + root: List[AggregateFSResourceMapSyncState] @classmethod def from_aggregate_map( @@ -74,7 +74,7 @@ def from_aggregate_map( res_intersection = set(lm) & set(rm) - return cls.parse_obj( + return cls.model_validate( [ AggregateFSResourceMapSyncState.from_resource_maps( local_resource_map=lm[res_id], remote_resource_map=rm[res_id] diff --git a/hydroshare_on_jupyter/models/api_models.py b/hydroshare_on_jupyter/models/api_models.py index 8f02f2ad..b4c58db1 100644 --- a/hydroshare_on_jupyter/models/api_models.py +++ b/hydroshare_on_jupyter/models/api_models.py @@ -1,12 +1,15 @@ from pydantic import ( + RootModel, BaseModel, Field, StrictStr, StrictBool, constr, - validator, + field_validator, + ConfigDict, + StringConstraints, ) -from typing import List, Union +from typing import List, Union, Literal, Any, Annotated from hsclient import Token from .resource_type_enum import ResourceTypeEnum @@ -15,8 +18,10 @@ class ModelNoExtra(BaseModel): """does not permit extra fields""" - class Config: - extra = "forbid" + model_config = ConfigDict(extra="forbid") + # TODO: cleanup - also cleanup imports above + # class Config: + # extra = "forbid" class Boolean(BaseModel): @@ -39,32 +44,42 @@ class OAuthCredentials(ModelNoExtra): CredentialTypes = Union[StandardCredentials, OAuthCredentials] - -class Credentials(BaseModel): - __root__: CredentialTypes = Field(...) - - def dict( - self, - *, - include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - by_alias: bool = False, - skip_defaults: bool = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False - ) -> "DictStrAny": - d = super().dict( - include=include, - exclude=exclude, - by_alias=by_alias, - skip_defaults=skip_defaults, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - # return contents of root key dropping it in the process - return d["__root__"] +Credentials = RootModel[CredentialTypes] + +# TODO: cleanup +# class Credentials(RootModel, BaseModel): +# root: CredentialTypes = Field(...) +# +# def model_dump( +# self, +# *, +# mode: Literal['json', 'python'] | str = 'python', +# include: "IncEx" = None, +# exclude: "IncEx" = None, +# context: dict[str, Any] | None = None, +# by_alias: bool = False, +# exclude_unset: bool = False, +# exclude_defaults: bool = False, +# exclude_none: bool = False, +# round_trip: bool = False, +# warnings: bool | Literal['none', 'warn', 'error'] = True, +# serialize_as_any: bool = False, +# ) -> dict[str, Any]: +# d = super().model_dump( +# mode=mode, +# include=include, +# exclude=exclude, +# context=context, +# by_alias=by_alias, +# exclude_unset=exclude_unset, +# exclude_defaults=exclude_defaults, +# exclude_none=exclude_none, +# round_trip=round_trip, +# warnings=warnings, +# serialize_as_any=serialize_as_any, +# ) +# # return contents of root key dropping it in the process +# return d["root"] class Success(BaseModel): @@ -83,18 +98,18 @@ class ResourceMetadata(BaseModel): authors: List[str] = Field(...) # NOTE: remove once https://github.com/hydroshare/hsclient/issues/23 has been resolved - @validator("authors", pre=True, always=True) + @field_validator("authors", mode="before") def handle_null_author(cls, v): return v or [] - @validator("creator", pre=True, always=True) + @field_validator("creator", mode="before") def handle_null_creator(cls, v): return "" if v is None else v -class CollectionOfResourceMetadata(BaseModel): +class CollectionOfResourceMetadata(RootModel): # from https://github.com/samuelcolvin/pydantic/issues/675#issuecomment-513029543 - __root__: List[ResourceMetadata] + root: List[ResourceMetadata] class ResourceCreationRequest(BaseModel): @@ -110,9 +125,14 @@ class ResourceCreationRequest(BaseModel): resource_type: ResourceTypeEnum +ResFileType = Annotated[str, StringConstraints(pattern=r"^((?!~|\.{2}).)*$")] + + class ResourceFiles(BaseModel): # str in list cannot contain .. or ~ - files: List[constr(regex=r"^((?!~|\.{2}).)*$")] = Field(...) + files: List[ResFileType] = Field(...) + + model_config = ConfigDict(regex_engine='python-re') class DataDir(BaseModel): diff --git a/hydroshare_on_jupyter/models/oauth.py b/hydroshare_on_jupyter/models/oauth.py index ddf6cafb..9c90d112 100644 --- a/hydroshare_on_jupyter/models/oauth.py +++ b/hydroshare_on_jupyter/models/oauth.py @@ -1,32 +1,44 @@ -from pydantic import BaseModel +from pydantic import BaseModel, RootModel from hsclient import Token # typing imports -from typing import Tuple, Union +from typing import Tuple, Union, Literal, Any, Optional -class OAuthFile(BaseModel): - __root__: Tuple[Token, str] +OAuthFile = RootModel[Tuple[Token, str]] - def dict( - self, - *, - include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None, - by_alias: bool = False, - skip_defaults: bool = None, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False - ) -> "DictStrAny": - d = super().dict( - include=include, - exclude=exclude, - by_alias=by_alias, - skip_defaults=skip_defaults, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) - # drop __root__, return only inner model - return d["__root__"] +# TODO: cleanup - also fix the imports above +# class OAuthFile(RootModel, BaseModel): +# root: Tuple[Token, str] +# def model_dump( +# self, +# *, +# mode: Literal['json', 'python'] | str = 'python', +# include: "IncEx" = None, +# exclude: "IncEx" = None, +# context: dict[str, Any] | None = None, +# by_alias: bool = False, +# exclude_unset: bool = False, +# exclude_defaults: bool = False, +# exclude_none: bool = False, +# round_trip: bool = False, +# warnings: bool | Literal['none', 'warn', 'error'] = True, +# serialize_as_any: bool = False, +# ) -> dict[str, Any]: +# d = super().model_dump( +# mode=mode, +# include=include, +# exclude=exclude, +# context=context, +# by_alias=by_alias, +# exclude_unset=exclude_unset, +# exclude_defaults=exclude_defaults, +# exclude_none=exclude_none, +# round_trip=round_trip, +# warnings=warnings, +# serialize_as_any=serialize_as_any, +# ) +# # drop __root__, return only inner model +# print(d) +# root, _ = d +# return self.root diff --git a/hydroshare_on_jupyter/server.py b/hydroshare_on_jupyter/server.py index d24468f8..a7ede6ac 100644 --- a/hydroshare_on_jupyter/server.py +++ b/hydroshare_on_jupyter/server.py @@ -13,7 +13,9 @@ from pydantic import ValidationError from jupyter_server.base.handlers import JupyterHandler -from notebook.utils import url_path_join +# TODO: cleanup +# from notebook.utils import url_path_join +from jupyter_server.utils import url_path_join from typing import Union, List, Optional from tempfile import TemporaryDirectory from zipfile import ZipFile @@ -192,7 +194,7 @@ def prepare(self): pass def get(self): - self.write(DataDir(data_directory=str(self.data_path)).dict()) + self.write(DataDir(data_directory=str(self.data_path)).model_dump()) class ServerRootHandler(HeadersMixIn, BaseRequestHandler): @@ -206,7 +208,7 @@ def prepare(self): def get(self): server_root = Path(self.settings["server_root_dir"]).expanduser().resolve() - self.write(ServerRootDir(server_root=str(server_root)).dict()) + self.write(ServerRootDir(server_root=str(server_root)).model_dump()) class UsingOAuth(MutateSessionMixIn, HeadersMixIn, BaseRequestHandler): @@ -221,7 +223,7 @@ def prepare(self): def get(self): if self.oauth_creds: - self.write(self.oauth_creds.dict()) + self.write(self.oauth_creds.model_dump()) else: # TODO: In the future, a model denoting that oauth is not enabled should be returned instead. empty = { @@ -231,7 +233,7 @@ def get(self): "token_type": "", }, } - self.write(OAuthCredentials.parse_obj(empty).dict()) + self.write(OAuthCredentials.model_validate(empty).model_dump()) class LoginHandler(MutateSessionMixIn, HeadersMixIn, BaseRequestHandler): @@ -273,7 +275,7 @@ def post(self): # NOTE: A user can login and then try to login to another account, but if they # do not use the DELETE method first, they will still be signed into the first # account bc of the `user` cookie - credentials = Credentials.parse_raw(self.request.body.decode("utf-8")) + credentials = Credentials.model_validate_json(self.request.body.decode("utf-8")) self.log.info("parsed user credentials") self.successful_login = True @@ -291,10 +293,10 @@ def post(self): self.set_status(HTTPStatus.INTERNAL_SERVER_ERROR) # 500 # self.successful_login initialized to False in `prepare` - self.write(Success(success=self.successful_login).dict()) + self.write(Success(success=self.successful_login).model_dump()) def _create_session(self, credentials: Credentials) -> None: - hs = HydroShare(**credentials.dict()) + hs = HydroShare(**credentials.model_dump()) user_info = hs.my_user_info() user_id = int(user_info["id"]) username = user_info["username"] @@ -355,7 +357,7 @@ def get(self): resources = list(session.session.search(edit_permission=True)) # Marshall hsclient representation into CollectionOfResourceMetadata - self.write(CollectionOfResourceMetadata.parse_obj(resources).json()) + self.write(CollectionOfResourceMetadata.model_validate(resources).model_dump_json()) class ListHydroShareResourceFiles(HeadersMixIn, BaseRequestHandler): @@ -381,7 +383,7 @@ def get(self, resource_id: str): ] # Marshall hsclient representation into CollectionOfResourceMetadata - self.write(ResourceFiles(files=files).json()) + self.write(ResourceFiles(files=files).model_dump_json()) def on_finish(self) -> None: # emit event to notify that a local resource has been listed. if there is local copy, it @@ -516,7 +518,7 @@ def post(self, resource_id: str): # TODO: Add the ability to version data try: # Marshall request body - files = ResourceFiles.parse_raw(self.request.body.decode("utf-8")).files + files = ResourceFiles.model_validate_json(self.request.body.decode("utf-8")).files except ValidationError: # fail fast @@ -596,7 +598,7 @@ def _truncate_baggit_prefix(file_path: str) -> str: if baggit_prefix_match is not None: # left-truncate baggit prefix path - file_path = file_path[baggit_prefix_match.end() :] + file_path = file_path[baggit_prefix_match.end():] return file_path @@ -618,5 +620,5 @@ class UserInfoHandler(HeadersMixIn, BaseRequestHandler): def get(self): """Gets the user's information (name, email, etc) from HydroShare""" session = self.get_session() - user = session.session.user(session.id).dict() + user = session.session.user(session.id).model_dump() self.write(user) diff --git a/setup.py b/setup.py index 1c2ba5b3..de114d74 100644 --- a/setup.py +++ b/setup.py @@ -37,11 +37,12 @@ # Package dependency requirements REQUIREMENTS = [ - "hsclient>=0.2.0", + "hsclient>=1.0.1", "jupyterlab", "notebook", "requests", - "pydantic[dotenv]", + "pydantic==2.7.*", + "pydantic-settings==2.5.*", "watchdog", ] diff --git a/tests/test_aggregate_fs_map.py b/tests/test_aggregate_fs_map.py index ae1ac2e1..da2fd8f8 100644 --- a/tests/test_aggregate_fs_map.py +++ b/tests/test_aggregate_fs_map.py @@ -2,7 +2,8 @@ import os from hsclient import HydroShare from pathlib import Path -from pydantic import BaseSettings, Field +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict from hydroshare_on_jupyter.lib.filesystem.aggregate_fs_map import AggregateFSMap @@ -13,18 +14,20 @@ def get_env_file_path() -> Path: class HydroShareCreds(BaseSettings): - username: str = Field(..., env="HYDRO_USERNAME") - password: str = Field(..., env="HYDRO_PASSWORD") + username: str = Field(..., validation_alias="HYDRO_USERNAME") + password: str = Field(..., validation_alias="HYDRO_PASSWORD") - class Config: - env_file = get_env_file_path() - env_file_encoding = "utf-8" + model_config = SettingsConfigDict(env_file=get_env_file_path(), env_file_encoding="utf-8") + # TODO: cleanup - also clean up imports above + # class Config: + # env_file = get_env_file_path() + # env_file_encoding = "utf-8" @pytest.fixture def hydroshare(): creds = HydroShareCreds() - hs = HydroShare(**creds.dict()) + hs = HydroShare(**creds.model_dump()) return hs diff --git a/tests/test_api_models.py b/tests/test_api_models.py index 389ac25f..df3b8d5d 100644 --- a/tests/test_api_models.py +++ b/tests/test_api_models.py @@ -20,15 +20,15 @@ def resource_metadata(): def test_collection_of_resource_metadata(resource_metadata): metadata = resource_metadata - metadata_dict = metadata.dict() + metadata_dict = metadata.model_dump() - assert m.CollectionOfResourceMetadata.parse_obj([metadata, metadata, metadata]) - assert m.CollectionOfResourceMetadata.parse_obj([metadata]) + assert m.CollectionOfResourceMetadata.model_validate([metadata, metadata, metadata]) + assert m.CollectionOfResourceMetadata.model_validate([metadata]) # test as dictionaries - assert m.CollectionOfResourceMetadata.parse_obj( + assert m.CollectionOfResourceMetadata.model_validate( [metadata_dict, metadata_dict, metadata_dict] ) - assert m.CollectionOfResourceMetadata.parse_obj([metadata_dict]) + assert m.CollectionOfResourceMetadata.model_validate([metadata_dict]) def test_collection_of_resource_metadata_raises(resource_metadata): @@ -39,8 +39,8 @@ def test_collection_of_resource_metadata_raises(resource_metadata): "resource_title": "title", } - with pytest.raises(pydantic.error_wrappers.ValidationError): - assert m.CollectionOfResourceMetadata.parse_obj([metadata, metadata_subset]) + with pytest.raises(pydantic.ValidationError): + assert m.CollectionOfResourceMetadata.model_validate([metadata, metadata_subset]) def test_resource_files_should_pass(): diff --git a/tests/test_config.py b/tests/test_config.py index d2b06354..0dfe77e2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,13 +11,13 @@ def test_config_file(log_dir: str): with TemporaryDirectory() as temp: log = Path(temp) / log_dir - ConfigFile(data_path=temp, log_path=log) + ConfigFile(data=temp, log=log) def test_config_creatifile(): with TemporaryDirectory() as temp: log = Path(temp) / "dir_that_does_not_exist" / "logs" - ConfigFile(data_path=temp, log_path=log) + ConfigFile(data=temp, log=log) def test_config_log_is_file(): @@ -25,7 +25,7 @@ def test_config_log_is_file(): with TemporaryDirectory() as temp: log = Path(temp) / "logs" log.touch() - ConfigFile(data_path=temp, log_path=log) + ConfigFile(data=temp, log=log) def test_config_using_env_vars(monkeypatch): @@ -66,6 +66,7 @@ def oauth_data(): "token_type": "Bearer", "refresh_token": "some_fake_token", "scope": "scope", + "state": "", "expires_in": 2592000, }, "some_fake_token", @@ -84,5 +85,5 @@ def oauth_file(oauth_data) -> Path: def test_config_oauth(oauth_file, oauth_data): - o = ConfigFile(oauth_path=str(oauth_file)) - assert o.oauth_path.__root__[0].access_token == oauth_data[0]["access_token"] + o = ConfigFile(oauth=str(oauth_file)) + assert o.oauth_path.root[0].access_token == oauth_data[0]["access_token"] diff --git a/tests/test_oauth_json_model.py b/tests/test_oauth_json_model.py index b5a7bbee..cfdd6b27 100644 --- a/tests/test_oauth_json_model.py +++ b/tests/test_oauth_json_model.py @@ -17,5 +17,5 @@ def testing_data(): def test_it_works(testing_data): - o = OAuthFile.parse_obj(testing_data) - o.dict()[1] == testing_data[1] + o = OAuthFile.model_validate(testing_data) + assert o.model_dump()[1] == testing_data[1] diff --git a/tests/test_pathlib_utils.py b/tests/test_pathlib_utils.py index 002ce10a..89838798 100644 --- a/tests/test_pathlib_utils.py +++ b/tests/test_pathlib_utils.py @@ -17,7 +17,15 @@ def test_expand_and_resolve(test, validation, user): # unix-like os use `HOME`. Windows use `USERPROFILE` os.environ["HOME"] = os.environ["USERPROFILE"] = user - assert str(pathlib_utils.expand_and_resolve(test)) == validation + path = pathlib_utils.expand_and_resolve(test) + # remove drive letter if it exists in path (windows) for comparison + if path.drive: + # this is the case on windows + path = path.relative_to(path.drive) + # change the path to unix-like + path = path.as_posix() + + assert str(path) == validation TEST_IS_DESCENDANT = [ diff --git a/tests/test_server.py b/tests/test_server.py index e93ccff5..f6cef05a 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,17 +1,18 @@ +import json +from http.cookies import SimpleCookie, Morsel +from typing import Union + +import pytest +from hsclient import HydroShare from tornado.httpclient import HTTPRequest, HTTPResponse from tornado.httputil import HTTPHeaders -from hsclient import HydroShare + from hydroshare_on_jupyter.__main__ import get_test_app from hydroshare_on_jupyter.hydroshare_resource_cache import ( HydroShareWithResourceCache, ) from hydroshare_on_jupyter.server import LocalResourceEntityHandler from hydroshare_on_jupyter.session import _SessionSyncSingleton -import json -import pytest -from dataclasses import dataclass -from http.cookies import SimpleCookie, Morsel -from typing import Union def my_user_info_mock(*args, **kwargs):