Skip to content

Commit

Permalink
Refactor target site (#53)
Browse files Browse the repository at this point in the history
* initial config

* passes tests, but does it work?

* rename config funcs and bugfix

* initial config

* passes tests, but does it work?

* rename config funcs and bugfix

* test and continue site_utils work

* add env variable for default datastack name

* tests and tweaks
  • Loading branch information
ceesem authored Nov 20, 2024
1 parent b278a71 commit 8b7a45f
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 138 deletions.
23 changes: 22 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ authors = [

dependencies = [
"python-box",
"caveclient>=5.22.0",
"caveclient>=6.4.1",
"ipython",
"neuroglancer >= 2.40.1",
"numpy>=1.11.0",
Expand Down Expand Up @@ -51,9 +51,30 @@ include = [
"/src",
]

[tool.hatch.envs.default]
python = "3.12"
installer = "uv"

[tool.hatch.envs.nglui-nb]
python = "3.12"
installer = "uv"
extra-dependencies = [
"ipykernel",
]

[tool.hatch.envs.hatch-test]
extra-dependencies = [
"pytest",
"pytest-mock",
"pytest-cov",
"responses",
]


[[tool.hatch.envs.hatch-test.matrix]]
python = ["3.9", "3.10", "3.11", "3.12"]


[tool.hatch.envs.docs]
dependencies = [
"mkdocs",
Expand Down
2 changes: 1 addition & 1 deletion src/nglui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from nglui.easyviewer import EasyViewer
from . import parser, segmentprops, statebuilder

from . import parser, segmentprops, statebuilder

__version__ = "3.5.2"
26 changes: 23 additions & 3 deletions src/nglui/easyviewer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
from typing import Optional

from ..site_utils import get_default_config, is_mainline
from .ev_base import EasyViewerMainline, EasyViewerSeunglab
from ..target_utils import is_mainline


def EasyViewer(
target_site="seunglab",
target_site: Optional[str] = None,
config_key: str = "default",
):
if not is_mainline(target_site) or target_site is None:
"""Factory function to return the appropriate EasyViewer object based on the target site.
Parameters
----------
target_site : Optional[str], optional
Type of target site, either "seunglab" or ""spelunker/mainline/"cave-explorer", by default None.
If None, uses the configuration setting.
config_key : str, optional
Name of the configuration to use, by default "default"
Returns
-------
EasyViewer
EasyViewer object based on the target site to aid in building states.
"""
if target_site is None:
target_site = get_default_config(config_key)["target_site"]
if not is_mainline(target_site):
return EasyViewerSeunglab()
elif is_mainline(target_site):
return EasyViewerMainline()
Expand Down
9 changes: 6 additions & 3 deletions src/nglui/easyviewer/ev_base/seunglab.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

from numpy import integer, issubdtype, uint64, vstack

from ...site_utils import SEUNGLAB_NAMES, neuroglancer_url
from . import nglite as neuroglancer
from . import utils
from .base import SEGMENTATION_LAYER_TYPES, EasyViewerBase

SEUNGLAB_NAME = SEUNGLAB_NAMES[0]


class EasyViewerSeunglab(neuroglancer.UnsynchronizedViewer, EasyViewerBase):
def __init__(self, **kwargs):
Expand All @@ -32,7 +35,7 @@ def _AnnotationLayer(self, **kwargs):

def _SegmentationLayer(self, source, **kwargs):
if re.search(r"^graphene://", source) is not None:
source = utils.parse_graphene_header(source, "seunglab")
source = utils.parse_graphene_header(source, SEUNGLAB_NAME)
return neuroglancer.ChunkedgraphSegmentationLayer(source=source, **kwargs)
elif re.search(r"^precomputed://", source) is not None:
return neuroglancer.SegmentationLayer(source=source, **kwargs)
Expand Down Expand Up @@ -119,8 +122,8 @@ def as_url(
as_html: Optional[bool] = False,
link_text: Optional[str] = "Neuroglancer Link",
) -> str:
prefix = utils.neuroglancer_url(
prefix, "seunglab"
prefix = neuroglancer_url(
prefix, SEUNGLAB_NAME
) # 'seunglab' hard-coded because of file.
ngl_url = neuroglancer.to_url(self.state, prefix=prefix)
if as_html:
Expand Down
23 changes: 1 addition & 22 deletions src/nglui/easyviewer/ev_base/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@
import pandas as pd
import webcolors

from ...target_utils import (
from ...site_utils import (
is_mainline,
is_seunglab,
default_seunglab_neuroglancer_base,
default_mainline_neuroglancer_base,
MAINLINE_NAMES,
)


Expand Down Expand Up @@ -84,21 +81,3 @@ def _parse_to_mainline_imagery(qry):
return f"precomputed://middleauth+http:{qry.path}"
else:
return qry.geturl()


def neuroglancer_url(url, target_site):
"""
Check neuroglancer info to determine which kind of site a neuroglancer URL is.
"""
if url is not None:
return url
elif target_site is None:
return None
elif is_seunglab(target_site):
return default_seunglab_neuroglancer_base
elif is_mainline(target_site):
return default_mainline_neuroglancer_base
else:
raise ValueError(
f"Must specify either a URL or a target site (either 'seunglab' or one of {MAINLINE_NAMES})"
)
222 changes: 222 additions & 0 deletions src/nglui/site_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import logging
import os
from typing import Literal, Optional

import attrs
import caveclient

logger = logging.getLogger(__name__)

MAINLINE_NAMES = ["spelunker", "mainline", "cave-explorer"]
SEUNGLAB_NAMES = ["seunglab"]
default_seunglab_neuroglancer_base = "https://neuromancer-seung-import.appspot.com/"
default_mainline_neuroglancer_base = "https://spelunker.cave-explorer.org/"

DEFAULT_TARGET_SITE = os.environ.get("NGLUI_DEFAULT_TARGET_SITE", "seunglab")
DEFAULT_URL = os.environ.get("NGLUI_DEFAULT_TARGET_URL", None)
DEFAULT_DATASTACK_NAME = os.environ.get("NGLUI_DEFAULT_DATASTACK_NAME", None)

__all__ = [
"is_mainline",
"is_seunglab",
"check_target_site",
"set_default_config",
"get_default_config",
"reset_default_config",
MAINLINE_NAMES,
SEUNGLAB_NAMES,
]


def is_mainline(target_name):
return target_name in MAINLINE_NAMES


def is_seunglab(target_name):
return target_name in SEUNGLAB_NAMES


def check_target_site(ngl_url, client):
"""
Check neuroglancer info to determine which kind of site a neuroglancer URL is.
"""
ngl_info = client.state.get_neuroglancer_info(ngl_url)
if len(ngl_info) == 0:
return SEUNGLAB_NAMES[0]
else:
return MAINLINE_NAMES[0]


@attrs.define
class NGLUIConfig:
target_site = attrs.field(type=str)
target_url = attrs.field(type=str)
seunglab_fallback_url = attrs.field(
default=default_seunglab_neuroglancer_base, type=str
)
mainline_fallback_url = attrs.field(
default=default_mainline_neuroglancer_base, type=str
)
caveclient = attrs.field(default=None)
datastack_name = attrs.field(default=DEFAULT_DATASTACK_NAME, type=str)

def __attrs_post_init__(self):
# validate target site
if (
self.target_site not in MAINLINE_NAMES + SEUNGLAB_NAMES
and self.target_site is not None
):
raise ValueError(
f"target_site must be one of {MAINLINE_NAMES + SEUNGLAB_NAMES}"
)
# Set and validate caveclient if info is provided
if self.caveclient is None and self.datastack_name is not None:
try:
self.caveclient = caveclient.CAVEclient(self.datastack_name)
except Exception as e:
msg = f"Could not create CAVEclient with datastack_name {self.datastack_name}: {e}"
logger.error(msg)
if self.caveclient is not None and self.datastack_name is None:
self.datastack_name = self.caveclient.datastack_name
if self.datastack_name is not None and self.caveclient is not None:
if self.caveclient.datastack_name != self.datastack_name:
raise ValueError(
f"CAVEclient datastack_name {self.caveclient.datastack_name} does not match provided datastack_name {self.datastack_name}"
)

# Set and validate target_url if target_site is provided
if self.target_url is not None and self.target_site is None:
self.target_site = check_target_site(self.target_url, self.caveclient)
if self.target_site is not None and self.target_url is None:
if is_seunglab(self.target_site):
self.target_url = self.seunglab_fallback_url
else:
self.target_url = self.mainline_fallback_url
if self.target_site is not None and self.target_url is not None:
if self.caveclient is not None:
url_based_site = check_target_site(self.target_url, self.caveclient)
if is_mainline(url_based_site) != is_mainline(self.target_site):
logging.warning(
f"target_site {self.target_site} does not match target_url {self.target_url}. Assuming manual setting is correct."
)


default_config = NGLUIConfig(
target_site=DEFAULT_TARGET_SITE,
target_url=DEFAULT_URL,
)
default_key = "default"
NGL_CONFIG = {default_key: default_config} # type: dict[str, NGLUIConfig]


def get_default_config(config_key: str = None):
"""Get the current configuration for nglui viewers and statebuilders.
Parameters
----------
config_key : str, optional
Key for the configuration setting, by default "default"
"""
if config_key is None:
config_key = default_key
return attrs.asdict(NGL_CONFIG[config_key])


def set_default_config(
target_site: Optional[
Literal["seunglab", "mainline", "cave-explorer", "spelunker"]
] = None,
target_url: str = None,
seunglab_fallback_url: str = None,
mainline_fallback_url: str = None,
datastack_name: str = None,
caveclient: "caveclient.CAVEclient" = None,
url_from_client: bool = False,
config_key: str = "default",
):
"""Set default configuration for nglui viewers and statebuilders.
Parameters
----------
target_site : str, optional
Target site for the neuroglancer viewer, by default None
target_url : str, optional
Target URL for the neuroglancer viewer, by default None
seunglab_url : str, optional
URL for the seunglab neuroglancer viewer, by default None
mainline_url : str, optional
URL for the mainline neuroglancer viewer, by default None
datastack_name : str, optional
Name of the datastack, by default None
caveclient : caveclient.CAVEclient, optional
CAVEclient object, by default None.
"""
if config_key in NGL_CONFIG:
curr_config = get_default_config(config_key)
else:
curr_config = attrs.asdict(default_config)
if target_site is not None or target_url is not None:
curr_config["target_site"] = target_site
curr_config["target_url"] = target_url
if seunglab_fallback_url is not None:
curr_config["seunglab_fallback_url"] = seunglab_fallback_url
if mainline_fallback_url is not None:
curr_config["mainline_fallback_url"] = mainline_fallback_url
if caveclient is not None or datastack_name is not None:
curr_config["caveclient"] = caveclient
curr_config["datastack_name"] = datastack_name
if url_from_client:
curr_config["target_site"] = None
curr_config["target_url"] = caveclient.info.viewer_site()
NGL_CONFIG[config_key] = NGLUIConfig(**curr_config)


def reset_config(
config_key: str = "default",
):
"""Reset the configuration for nglui viewers and statebuilders to the default.
Parameters
----------
config_key : str, optional
Key for the configuration setting, by default "default"
"""
NGL_CONFIG[config_key] = default_config


def neuroglancer_url(
url: Optional[str] = None,
target_site: Optional[str] = None,
config_key: str = "default",
):
"""
Check neuroglancer info to determine which kind of site a neuroglancer URL is.
If either url or target_site are provided, it will use these values, looking up target site
from the fallback values in the config. Otherwise, it falls back to the value of
"target_url" in the config.
Parameters
----------
url : str, optional
URL to check, by default None
target_site : str, optional
Target site to check, by default None
Returns
-------
str
URL of the neuroglancer viewer
"""
if url is not None:
return url
if target_site is None:
url = get_default_config(config_key)["target_url"]
elif is_seunglab(target_site):
url = get_default_config(config_key)["seunglab_fallback_url"]
else:
url = get_default_config(config_key)["mainline_fallback_url"]
return url


def reset_default_config():
NGL_CONFIG[default_key] = default_config
2 changes: 1 addition & 1 deletion src/nglui/statebuilder/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from caveclient.endpoints import fallback_ngl_endpoint
from IPython.display import HTML

from ..easyviewer.ev_base.utils import neuroglancer_url
from ..site_utils import neuroglancer_url
from .layers import (
AnnotationLayerConfig,
ImageLayerConfig,
Expand Down
Loading

0 comments on commit 8b7a45f

Please sign in to comment.