diff --git a/packages/evo-blockmodels/docs/examples/quickstart.ipynb b/packages/evo-blockmodels/docs/examples/quickstart.ipynb
index 4b16a434..b9e9e13a 100644
--- a/packages/evo-blockmodels/docs/examples/quickstart.ipynb
+++ b/packages/evo-blockmodels/docs/examples/quickstart.ipynb
@@ -17,12 +17,13 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"manager = await ServiceManagerWidget.with_auth_code(\n",
" client_id=\"your-client-id\",\n",
" cache_location=\"./notebook-data\",\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -190,7 +191,7 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": "evo-sdk",
"language": "python",
"name": "python3"
},
diff --git a/packages/evo-colormaps/docs/examples/quickstart.ipynb b/packages/evo-colormaps/docs/examples/quickstart.ipynb
index d575e972..0ed9b881 100644
--- a/packages/evo-colormaps/docs/examples/quickstart.ipynb
+++ b/packages/evo-colormaps/docs/examples/quickstart.ipynb
@@ -17,12 +17,13 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"manager = await ServiceManagerWidget.with_auth_code(\n",
" client_id=\"your-client-id\",\n",
" cache_location=\"./notebook-data\",\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/packages/evo-compute/docs/examples/quickstart.ipynb b/packages/evo-compute/docs/examples/quickstart.ipynb
index 75331903..6061761a 100644
--- a/packages/evo-compute/docs/examples/quickstart.ipynb
+++ b/packages/evo-compute/docs/examples/quickstart.ipynb
@@ -21,11 +21,12 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"manager = await ServiceManagerWidget.with_auth_code(\n",
" client_id=\"your-client-id\", cache_location=\"./notebook-data\"\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/packages/evo-files/docs/examples/quickstart.ipynb b/packages/evo-files/docs/examples/quickstart.ipynb
index b60c5c68..1d67284b 100644
--- a/packages/evo-files/docs/examples/quickstart.ipynb
+++ b/packages/evo-files/docs/examples/quickstart.ipynb
@@ -17,7 +17,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"manager = await ServiceManagerWidget.with_auth_code(\n",
" client_id=\"your-client-id\",\n",
diff --git a/packages/evo-objects/docs/examples/quickstart.ipynb b/packages/evo-objects/docs/examples/quickstart.ipynb
index 5255c1f2..57de7a40 100644
--- a/packages/evo-objects/docs/examples/quickstart.ipynb
+++ b/packages/evo-objects/docs/examples/quickstart.ipynb
@@ -17,11 +17,12 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"manager = await ServiceManagerWidget.with_auth_code(\n",
" client_id=\"your-client-id\", cache_location=\"./notebook-data\"\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -182,7 +183,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"# Use the data client to upload all data referenced by the pointset\n",
"await data_client.upload_referenced_data(sample_pointset, fb=FeedbackWidget(\"Uploading data\"))\n",
@@ -263,7 +264,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"downloaded_object = await object_client.download_object_by_path(\"sdk/v2/sample-pointset.json\")\n",
"metadata = downloaded_object.metadata\n",
diff --git a/packages/evo-sdk-common/docs/examples/notebook-utilities.ipynb b/packages/evo-sdk-common/docs/examples/notebook-utilities.ipynb
index fb403aa9..9846e725 100644
--- a/packages/evo-sdk-common/docs/examples/notebook-utilities.ipynb
+++ b/packages/evo-sdk-common/docs/examples/notebook-utilities.ipynb
@@ -43,7 +43,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"manager = await ServiceManagerWidget.with_auth_code(\n",
" client_id=\"your-client-id\",\n",
@@ -113,7 +113,7 @@
"source": [
"import time\n",
"\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"fb = FeedbackWidget(\"Loading...\")\n",
"\n",
diff --git a/packages/evo-sdk-common/src/evo/notebooks/__init__.py b/packages/evo-sdk-common/src/evo/notebooks/__init__.py
deleted file mode 100644
index fdd679ba..00000000
--- a/packages/evo-sdk-common/src/evo/notebooks/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright © 2025 Bentley Systems, Incorporated
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-# http://www.apache.org/licenses/LICENSE-2.0
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from .widgets import FeedbackWidget, HubSelectorWidget, OrgSelectorWidget, ServiceManagerWidget, WorkspaceSelectorWidget
-
-__all__ = [
- "FeedbackWidget",
- "HubSelectorWidget",
- "OrgSelectorWidget",
- "ServiceManagerWidget",
- "WorkspaceSelectorWidget",
-]
diff --git a/packages/evo-sdk-common/src/evo/notebooks/assets/EvoBadgeCharcoal_FV.png b/packages/evo-sdk-common/src/evo/notebooks/assets/EvoBadgeCharcoal_FV.png
deleted file mode 100644
index e7d74099..00000000
Binary files a/packages/evo-sdk-common/src/evo/notebooks/assets/EvoBadgeCharcoal_FV.png and /dev/null differ
diff --git a/packages/evo-sdk-common/src/evo/notebooks/assets/loading.gif b/packages/evo-sdk-common/src/evo/notebooks/assets/loading.gif
deleted file mode 100644
index a718bd89..00000000
Binary files a/packages/evo-sdk-common/src/evo/notebooks/assets/loading.gif and /dev/null differ
diff --git a/packages/evo-sdk-common/src/evo/notebooks/widgets.py b/packages/evo-sdk-common/src/evo/notebooks/widgets.py
deleted file mode 100644
index 154f3150..00000000
--- a/packages/evo-sdk-common/src/evo/notebooks/widgets.py
+++ /dev/null
@@ -1,491 +0,0 @@
-# Copyright © 2025 Bentley Systems, Incorporated
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-# http://www.apache.org/licenses/LICENSE-2.0
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from __future__ import annotations
-
-import asyncio
-import contextlib
-from collections.abc import Iterator
-from typing import Any, Generic, TypeVar, cast
-from uuid import UUID
-
-import ipywidgets as widgets
-from aiohttp.typedefs import StrOrURL
-from IPython.display import display
-
-from evo import logging
-from evo.aio import AioTransport
-from evo.common import APIConnector, BaseAPIClient, Environment
-from evo.common.exceptions import UnauthorizedException
-from evo.common.interfaces import IAuthorizer, ICache, IFeedback, ITransport
-from evo.discovery import Hub, Organization
-from evo.oauth import AnyScopes, EvoScopes, OAuthConnector
-from evo.service_manager import ServiceManager
-from evo.workspaces import Workspace
-
-from ._consts import (
- DEFAULT_BASE_URI,
- DEFAULT_CACHE_LOCATION,
- DEFAULT_DISCOVERY_URL,
- DEFAULT_REDIRECT_URL,
-)
-from ._helpers import FileName, build_button_widget, build_img_widget, init_cache
-from .authorizer import AuthorizationCodeAuthorizer
-from .env import DotEnv
-
-T = TypeVar("T")
-
-logger = logging.getLogger(__name__)
-
-__all__ = [
- "FeedbackWidget",
- "HubSelectorWidget",
- "OrgSelectorWidget",
- "ServiceManagerWidget",
- "WorkspaceSelectorWidget",
-]
-
-
-class DropdownSelectorWidget(widgets.HBox, Generic[T]):
- UNSELECTED: tuple[str, T]
-
- def __init__(self, label: str, env: DotEnv) -> None:
- self._env = env
- self.dropdown_widget = widgets.Dropdown(
- options=[self.UNSELECTED],
- description=label,
- value=self.UNSELECTED[1],
- layout=widgets.Layout(margin="5px 5px 5px 5px", align_self="flex-start"),
- )
- self.dropdown_widget.disabled = True
- self.dropdown_widget.observe(self._update_selected, names="value")
-
- self._loading_widget = build_img_widget("loading.gif")
- self._loading_widget.layout.display = "none"
-
- super().__init__([self.dropdown_widget, self._loading_widget])
-
- def _get_options(self) -> list[tuple[str, T]]:
- raise NotImplementedError("Subclasses must implement this method.")
-
- def _on_selected(self, value: T | None) -> None: ...
-
- @contextlib.contextmanager
- def _loading(self) -> Iterator[None]:
- self.dropdown_widget.disabled = True
- self._loading_widget.layout.display = "flex"
- try:
- yield
- finally:
- self._loading_widget.layout.display = "none"
- self.dropdown_widget.disabled = False
-
- def _update_selected(self, _: dict) -> None:
- self.selected = new_value = self.dropdown_widget.value
- self._on_selected(new_value if new_value != self.UNSELECTED[1] else None)
-
- def refresh(self) -> None:
- logger.debug(f"Refreshing {self.__class__.__name__} options...")
- self.dropdown_widget.disabled = True
- selected = self.selected
- self.dropdown_widget.options = options = [self.UNSELECTED] + self._get_options()
- if len(options) == 2 and selected == self.UNSELECTED[1]:
- # Automatically select the only option if there is only one and no missing option was previously selected.
- self.selected = new_value = options[1][1]
- else:
- # Otherwise, ensure the selected option is still valid.
- for _, value in options:
- if value == selected:
- self.selected = new_value = selected
- break
- else:
- # If the selected option is no longer valid, reset to the unselected value.
- self.selected = new_value = self.UNSELECTED[1]
-
- # Make sure the new value is passed to the _on_selected method.
- self._on_selected(new_value if new_value != self.UNSELECTED[1] else None)
-
- # Disable the widget if there are no options to select.
- self.dropdown_widget.disabled = len(options) <= 1
-
- @classmethod
- def _serialize(cls, value: T) -> str:
- raise NotImplementedError("Subclasses must implement this method.")
-
- @classmethod
- def _deserialize(cls, value: str) -> T:
- raise NotImplementedError("Subclasses must implement this method.")
-
- @property
- def selected(self) -> T:
- value = self._env.get(f"{self.__class__.__name__}.selected", self._serialize(self.UNSELECTED[1]))
- return self._deserialize(value)
-
- @selected.setter
- def selected(self, value: T) -> None:
- self._env.set(f"{self.__class__.__name__}.selected", self._serialize(value))
- self.dropdown_widget.value = value
-
- @property
- def disabled(self) -> bool:
- return self.dropdown_widget.disabled
-
- @disabled.setter
- def disabled(self, value: bool) -> None:
- self.dropdown_widget.disabled = value
-
-
-_NULL_UUID = UUID(int=0)
-
-
-class _UUIDSelectorWidget(DropdownSelectorWidget[UUID]):
- @classmethod
- def _serialize(cls, value: UUID) -> str:
- return str(value)
-
- @classmethod
- def _deserialize(cls, value: str) -> UUID:
- return UUID(value)
-
-
-class OrgSelectorWidget(_UUIDSelectorWidget):
- UNSELECTED = ("Select Organisation", _NULL_UUID)
-
- def __init__(self, env: DotEnv, manager: ServiceManager) -> None:
- self._manager = manager
- super().__init__("Organisation", env)
-
- def _get_options(self) -> list[tuple[str, UUID]]:
- return [(org.display_name, org.id) for org in self._manager.list_organizations()]
-
- def _on_selected(self, value: UUID | None) -> None:
- self._manager.set_current_organization(value)
-
-
-class HubSelectorWidget(DropdownSelectorWidget[str]):
- UNSELECTED = ("Select Hub", "")
-
- def __init__(self, env: DotEnv, manager: ServiceManager, org_selector: OrgSelectorWidget) -> None:
- self._manager = manager
- super().__init__("Hub", env)
- org_selector.dropdown_widget.observe(self._on_org_selected, names="value")
-
- def _on_org_selected(self, _: dict) -> None:
- self.refresh()
-
- def _get_options(self) -> list[tuple[str, str]]:
- return [(hub.display_name, hub.code) for hub in self._manager.list_hubs()]
-
- def _on_selected(self, value: str | None) -> None:
- self._manager.set_current_hub(value)
-
- @classmethod
- def _serialize(cls, value: str) -> str:
- return value
-
- @classmethod
- def _deserialize(cls, value: str) -> str:
- return value
-
-
-class WorkspaceSelectorWidget(_UUIDSelectorWidget):
- UNSELECTED = ("Select Workspace", _NULL_UUID)
-
- def __init__(self, env: DotEnv, manager: ServiceManager, hub_selector: HubSelectorWidget) -> None:
- self._manager = manager
- super().__init__("Workspace", env)
- hub_selector.dropdown_widget.observe(self._on_hub_selected, names="value")
-
- async def refresh_workspaces(self) -> None:
- with self._loading():
- await self._manager.refresh_workspaces()
- self.refresh()
-
- def _on_hub_selected(self, _: dict) -> asyncio.Future:
- self.disabled = True
- return asyncio.ensure_future(self.refresh_workspaces())
-
- def _on_selected(self, value: UUID | None) -> None:
- self._manager.set_current_workspace(value)
-
- def _get_options(self) -> list[tuple[str, UUID]]:
- return [(ws.display_name, ws.id) for ws in self._manager.list_workspaces()]
-
-
-# Generic type variable for the client factory method.
-T_client = TypeVar("T_client", bound=BaseAPIClient)
-
-
-class ServiceManagerWidget(widgets.HBox):
- def __init__(self, transport: ITransport, authorizer: IAuthorizer, discovery_url: str, cache: ICache) -> None:
- """
- :param transport: The transport to use for API requests.
- :param authorizer: The authorizer to use for API requests.
- :param discovery_url: The URL of the Evo Discovery service.
- :param cache: The cache to use for storing tokens and other data.
- """
- self._authorizer = authorizer
- self._service_manager = ServiceManager(
- transport=transport,
- authorizer=authorizer,
- discovery_url=discovery_url,
- )
- self._cache = cache
- env = DotEnv(cache)
-
- self._btn = build_button_widget("Sign In")
- self._btn.on_click(self._on_click)
- self._org_selector = OrgSelectorWidget(env, self._service_manager)
- self._hub_selector = HubSelectorWidget(env, self._service_manager, self._org_selector)
- self._workspace_selector = WorkspaceSelectorWidget(env, self._service_manager, self._hub_selector)
-
- self._loading_widget = build_img_widget("loading.gif")
- self._loading_widget.layout.display = "none"
-
- self._prompt_area = widgets.Output()
- self._prompt_area.layout.display = "none"
-
- col_1 = widgets.VBox(
- [
- widgets.HBox([build_img_widget("EvoBadgeCharcoal_FV.png"), self._btn, self._loading_widget]),
- widgets.HBox([self._org_selector]),
- widgets.HBox([self._hub_selector]),
- widgets.HBox([self._workspace_selector]),
- ]
- )
- col_2 = widgets.VBox([self._prompt_area])
-
- super().__init__(
- [col_1, col_2],
- layout={
- "display": "flex",
- "flex_flow": "row",
- "justify_content": "space-between",
- "align_items": "center",
- },
- )
- display(self)
-
- @classmethod
- def with_auth_code(
- cls,
- client_id: str,
- base_uri: str = DEFAULT_BASE_URI,
- discovery_url: str = DEFAULT_DISCOVERY_URL,
- redirect_url: str = DEFAULT_REDIRECT_URL,
- client_secret: str | None = None,
- cache_location: FileName = DEFAULT_CACHE_LOCATION,
- oauth_scopes: AnyScopes = EvoScopes.all_evo | EvoScopes.offline_access,
- proxy: StrOrURL | None = None,
- ) -> ServiceManagerWidget:
- """Create a ServiceManagerWidget with an authorization code authorizer.
-
- To use it, you will need an OAuth client ID. See the documentation for information on how to obtain this:
- https://developer.seequent.com/docs/guides/getting-started/apps-and-tokens
-
- Chain this method with the login method to authenticate the user and obtain an access token:
-
- ```python
- manager = await ServiceManagerWidget.with_auth_code(client_id="your-client-id").login()
- ```
-
- :param client_id: The client ID to use for authentication.
- :param base_uri: The OAuth server base URI.
- :param discovery_url: The URL of the Evo Discovery service.
- :param redirect_url: The local URL to redirect the user back to after authorisation.
- :param client_secret: The client secret to use for authentication.
- :param cache_location: The location of the cache file.
- :param oauth_scopes: The OAuth scopes to request.
- :param proxy: The proxy URL to use for API requests.
-
- :returns: The new ServiceManagerWidget.
- """
- cache = init_cache(cache_location)
- transport = AioTransport(user_agent=client_id, proxy=proxy)
- authorizer = AuthorizationCodeAuthorizer(
- oauth_connector=OAuthConnector(
- transport=transport,
- base_uri=base_uri,
- client_id=client_id,
- client_secret=client_secret,
- ),
- redirect_url=redirect_url,
- scopes=oauth_scopes,
- env=DotEnv(cache),
- )
- return cls(transport, authorizer, discovery_url, cache)
-
- async def _login_with_auth_code(self, timeout_seconds: int) -> None:
- """Login using an authorization code authorizer.
-
- This method will attempt to reuse an existing token from the environment file. If no token is found, the user will
- be prompted to log in.
-
- :param timeout_seconds: The number of seconds to wait for the user to log in.
- """
- authorizer = cast(AuthorizationCodeAuthorizer, self._authorizer)
- if not await authorizer.reuse_token():
- await authorizer.login(timeout_seconds=timeout_seconds)
-
- async def login(self, timeout_seconds: int = 180) -> ServiceManagerWidget:
- """Authenticate the user and obtain an access token.
-
- Only the notebook authorizer implementations are supported by this method.
-
- This method returns the current instance of the ServiceManagerWidget to allow for method chaining.
-
- ```python
- manager = await ServiceManagerWidget.with_auth_code(client_id="your-client-id").login()
- ```
-
- :param timeout_seconds: The maximum time (in seconds) to wait for the authorisation process to complete.
-
- :returns: The current instance of the ServiceManagerWidget.
- """
- # Open the transport without closing it to avoid the overhead of opening it multiple times.
- await self._service_manager._transport.open()
- with self._loading():
- match self._authorizer:
- case AuthorizationCodeAuthorizer():
- await self._login_with_auth_code(timeout_seconds)
- case unknown:
- raise NotImplementedError(f"ServiceManagerWidget cannot login using {type(unknown).__name__}.")
-
- # Refresh the services after logging in.
- await self.refresh_services()
- return self
-
- @property
- def cache(self) -> ICache:
- return self._cache
-
- def _update_btn(self, signed_in: bool) -> None:
- if signed_in:
- self._btn.description = "Refresh Evo Services"
- else:
- self._btn.description = "Sign In"
-
- def _on_click(self, _: widgets.Button) -> asyncio.Future:
- return asyncio.ensure_future(self.refresh_services())
-
- @contextlib.contextmanager
- def _loading(self) -> Iterator[None]:
- self._btn.disabled = True
- self._loading_widget.layout.display = "flex"
- try:
- yield
- finally:
- self._loading_widget.layout.display = "none"
- self._btn.disabled = False
-
- @contextlib.contextmanager
- def _loading_services(self) -> Iterator[None]:
- self._org_selector.disabled = True
- self._hub_selector.disabled = True
- self._workspace_selector.disabled = True
- try:
- yield
- finally:
- self._org_selector.refresh()
- self._hub_selector.refresh()
-
- @contextlib.contextmanager
- def _prompt(self) -> Iterator[widgets.Output]:
- self._prompt_area.layout.display = "flex"
- try:
- yield self._prompt_area
- finally:
- self._prompt_area.layout.display = "none"
- self._prompt_area.clear_output()
-
- async def refresh_services(self) -> None:
- with self._loading():
- with self._loading_services():
- try:
- await self._service_manager.refresh_organizations()
- except UnauthorizedException: # Expired token or user not logged in.
- # Attempt to log in again.
- await self.login()
-
- # Try refresh the services again after logging in.
- await self._service_manager.refresh_organizations()
- await self._workspace_selector.refresh_workspaces()
- self._update_btn(True)
-
- @property
- def organizations(self) -> list[Organization]:
- return self._service_manager.list_organizations()
-
- @property
- def hubs(self) -> list[Hub]:
- return self._service_manager.list_hubs()
-
- @property
- def workspaces(self) -> list[Workspace]:
- return self._service_manager.list_workspaces()
-
- def get_connector(self) -> APIConnector:
- """Get an API connector for the currently selected hub.
-
- :returns: The API connector.
-
- :raises SelectionError: If no organization or hub is currently selected.
- """
- return self._service_manager.get_connector()
-
- def get_environment(self) -> Environment:
- """Get an environment with the currently selected organization, hub, and workspace.
-
- :returns: The environment.
-
- :raises SelectionError: If no organization, hub, or workspace is currently selected.
- """
- return self._service_manager.get_environment()
-
- def create_client(self, client_class: type[T_client], *args: Any, **kwargs: Any) -> T_client:
- """Create a client for the currently selected workspace.
-
- :param client_class: The class of the client to create.
-
- :returns: The new client.
-
- :raises SelectionError: If no organization, hub, or workspace is currently selected.
- """
- return self._service_manager.create_client(client_class, *args, **kwargs)
-
-
-class FeedbackWidget(IFeedback):
- """Simple feedback widget for displaying progress and messages to the user."""
-
- def __init__(self, label: str) -> None:
- """
- :param label: The label for the feedback widget.
- """
- label = widgets.Label(label)
- self._progress = widgets.FloatProgress(value=0, min=0, max=1, style={"bar_color": "#265C7F"})
- self._progress.layout.width = "400px"
- self._msg = widgets.Label("", style={"font_style": "italic"})
- self._widget = widgets.HBox([label, self._progress, self._msg])
- self._last_message = ""
- display(self._widget)
-
- def progress(self, progress: float, message: str | None = None) -> None:
- """Progress the feedback and update the text to message.
-
- This can raise an exception to cancel the current operation.
-
- :param progress: A float between 0 and 1 representing the progress of the operation as a percentage.
- :param message: An optional message to display to the user.
- """
- self._progress.value = progress
- self._progress.description = f"{progress * 100:5.1f}%"
- if message is not None:
- self._msg.value = message
diff --git a/packages/evo-widgets/LICENSE.md b/packages/evo-widgets/LICENSE.md
new file mode 100644
index 00000000..19ef6928
--- /dev/null
+++ b/packages/evo-widgets/LICENSE.md
@@ -0,0 +1,190 @@
+ Copyright © 2025 Bentley Systems, Incorporated.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/packages/evo-sdk-common/src/evo/notebooks/py.typed b/packages/evo-widgets/README.md
similarity index 100%
rename from packages/evo-sdk-common/src/evo/notebooks/py.typed
rename to packages/evo-widgets/README.md
diff --git a/packages/evo-widgets/pyproject.toml b/packages/evo-widgets/pyproject.toml
new file mode 100644
index 00000000..167a5c38
--- /dev/null
+++ b/packages/evo-widgets/pyproject.toml
@@ -0,0 +1,29 @@
+[project]
+name = "evo-widgets"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "anywidget>=0.9.21",
+ "traitlets>=5.14.3",
+ "evo-sdk-common>=0.1.0",
+]
+
+[project.optional-dependencies]
+aiohttp = [
+ "evo-sdk-common[aiohttp]>=0.1.0",
+]
+notebooks = [
+ "evo-sdk-common[notebooks]>=0.1.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.uv.sources]
+evo-sdk-common = { workspace = true }
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/evo"]
diff --git a/packages/evo-widgets/src/evo/widgets/__init__.py b/packages/evo-widgets/src/evo/widgets/__init__.py
new file mode 100644
index 00000000..0a4fd15a
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/__init__.py
@@ -0,0 +1 @@
+from .general import ServiceManagerWidget, FeedbackWidget
\ No newline at end of file
diff --git a/packages/evo-sdk-common/src/evo/notebooks/_consts.py b/packages/evo-widgets/src/evo/widgets/_consts.py
similarity index 100%
rename from packages/evo-sdk-common/src/evo/notebooks/_consts.py
rename to packages/evo-widgets/src/evo/widgets/_consts.py
diff --git a/packages/evo-sdk-common/src/evo/notebooks/_helpers.py b/packages/evo-widgets/src/evo/widgets/_helpers.py
similarity index 68%
rename from packages/evo-sdk-common/src/evo/notebooks/_helpers.py
rename to packages/evo-widgets/src/evo/widgets/_helpers.py
index 6611852e..99c3283c 100644
--- a/packages/evo-sdk-common/src/evo/notebooks/_helpers.py
+++ b/packages/evo-widgets/src/evo/widgets/_helpers.py
@@ -12,11 +12,8 @@
from pathlib import Path
from typing import TypeAlias
-import ipywidgets as widgets
-
from evo.common.utils import Cache
-from . import assets
from ._consts import DEFAULT_CACHE_LOCATION
FileName: TypeAlias = str | Path
@@ -36,21 +33,3 @@ def init_cache(cache_location: FileName = DEFAULT_CACHE_LOCATION) -> Cache:
ignorefile.write_text("*\n")
return cache
-
-def build_img_widget(filename: str) -> widgets.Image:
- image = assets.get(filename).read_bytes()
- return widgets.Image(
- value=image,
- format="png",
- layout=widgets.Layout(max_height="26px", margin="3px", align_self="center"),
- )
-
-
-def build_button_widget(text: str) -> widgets.Button:
- widget = widgets.Button(
- description=text,
- button_style="info",
- layout=widgets.Layout(margin="5px 5px 5px 5px", align_self="center"),
- )
- widget.style.button_color = "#265C7F"
- return widget
diff --git a/packages/evo-sdk-common/src/evo/notebooks/authorizer.py b/packages/evo-widgets/src/evo/widgets/authorizer.py
similarity index 100%
rename from packages/evo-sdk-common/src/evo/notebooks/authorizer.py
rename to packages/evo-widgets/src/evo/widgets/authorizer.py
diff --git a/packages/evo-sdk-common/src/evo/notebooks/env.py b/packages/evo-widgets/src/evo/widgets/env.py
similarity index 100%
rename from packages/evo-sdk-common/src/evo/notebooks/env.py
rename to packages/evo-widgets/src/evo/widgets/env.py
diff --git a/packages/evo-widgets/src/evo/widgets/general.py b/packages/evo-widgets/src/evo/widgets/general.py
new file mode 100644
index 00000000..5a32e303
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/general.py
@@ -0,0 +1,335 @@
+"""ServiceManagerWidget implementation using anywidget."""
+
+import asyncio
+from pathlib import Path
+from typing import Any
+from uuid import UUID
+
+import anywidget
+import traitlets
+
+from evo import logging
+from evo.aio import AioTransport
+from evo.common import APIConnector, BaseAPIClient, Environment
+from evo.common.exceptions import UnauthorizedException
+from evo.common.interfaces import IAuthorizer, ICache, IFeedback, ITransport
+from evo.discovery import Hub, Organization
+from evo.oauth import AnyScopes, EvoScopes, OAuthConnector
+from evo.service_manager import ServiceManager
+from evo.workspaces import Workspace
+from ._helpers import FileName, init_cache
+
+
+from ._consts import (
+ DEFAULT_BASE_URI,
+ DEFAULT_CACHE_LOCATION,
+ DEFAULT_DISCOVERY_URL,
+ DEFAULT_REDIRECT_URL,
+)
+from .authorizer import AuthorizationCodeAuthorizer
+from .env import DotEnv
+
+
+
+
+__all__ = ["ServiceManagerWidget"]
+
+
+class ServiceManagerWidget(anywidget.AnyWidget):
+ """Interactive widget for managing Evo services authentication and selection."""
+
+ _esm = Path(__file__).parent / "static" / "service_manager.js"
+ _css = Path(__file__).parent / "static" / "service_manager.css"
+
+ # Authentication state
+ signed_in = traitlets.Bool(False).tag(sync=True)
+ loading = traitlets.Bool(False).tag(sync=True)
+
+ # Dropdown options and selections
+ organizations = traitlets.List([]).tag(sync=True)
+ selected_org_id = traitlets.Unicode("").tag(sync=True)
+
+ hubs = traitlets.List([]).tag(sync=True)
+ selected_hub_code = traitlets.Unicode("").tag(sync=True)
+
+ workspaces = traitlets.List([]).tag(sync=True)
+ selected_workspace_id = traitlets.Unicode("").tag(sync=True)
+
+ # Button state
+ button_text = traitlets.Unicode("Sign In").tag(sync=True)
+
+ # Messages from frontend
+ action = traitlets.Unicode("").tag(sync=True)
+
+ def __init__(self, transport:ITransport, authorizer:IAuthorizer, discovery_url:str, cache:ICache, **kwargs):
+ """Initialize the ServiceManagerWidget.
+
+ Args:
+ transport: The transport to use for API requests
+ authorizer: The authorizer to use for API requests
+ discovery_url: The URL of the Evo Discovery service
+ cache: The cache to use for storing tokens and other data
+ """
+ super().__init__(**kwargs)
+
+ self._authorizer = authorizer
+ self._transport = transport
+ self._discovery_url = discovery_url
+ self._cache = cache
+ self._env = DotEnv(cache)
+ self._service_manager = None
+
+ # Initialize service manager if dependencies provided
+ if transport and authorizer and discovery_url:
+ self._service_manager = ServiceManager(
+ transport=transport,
+ authorizer=authorizer,
+ discovery_url=discovery_url,
+ )
+
+ # Observe action changes from frontend
+ self.observe(self._on_action, names=['action'])
+ self.observe(self._on_org_change, names=['selected_org_id'])
+ self.observe(self._on_hub_change, names=['selected_hub_code'])
+ self.observe(self._on_workspace_change, names=['selected_workspace_id'])
+
+ @classmethod
+ def with_auth_code(
+ cls,
+ client_id: str,
+ base_uri: str = DEFAULT_BASE_URI,
+ discovery_url: str = DEFAULT_DISCOVERY_URL,
+ redirect_url: str = DEFAULT_REDIRECT_URL,
+ client_secret: str | None = None,
+ cache_location: str = DEFAULT_CACHE_LOCATION,
+ oauth_scopes=None,
+ proxy=None,
+ ):
+ """Create a ServiceManagerWidget with an authorization code authorizer.
+
+ Args:
+ client_id: The client ID to use for authentication
+ base_uri: The OAuth server base URI
+ discovery_url: The URL of the Evo Discovery service
+ redirect_url: The local URL to redirect the user back to after authorisation
+ client_secret: The client secret to use for authentication
+ cache_location: The location of the cache file
+ oauth_scopes: The OAuth scopes to request
+ proxy: The proxy URL to use for API requests
+
+ Returns:
+ The new ServiceManagerWidget
+ """
+
+
+ # Initialize cache
+ cache = init_cache(cache_location)
+ ignorefile = cache.root / ".gitignore"
+ ignorefile.write_text("*\n")
+
+ # Set default scopes if not provided
+ if oauth_scopes is None:
+ oauth_scopes = EvoScopes.all_evo | EvoScopes.offline_access
+
+ transport = AioTransport(user_agent=client_id, proxy=proxy)
+ authorizer = AuthorizationCodeAuthorizer(
+ oauth_connector=OAuthConnector(
+ transport=transport,
+ base_uri=base_uri,
+ client_id=client_id,
+ client_secret=client_secret,
+ ),
+ redirect_url=redirect_url,
+ scopes=oauth_scopes,
+ env=DotEnv(cache),
+ )
+
+ return cls(transport, authorizer, discovery_url, cache)
+
+ async def login(self, timeout_seconds: int = 180):
+ """Authenticate the user and obtain an access token.
+
+ Args:
+ timeout_seconds: The maximum time (in seconds) to wait for authorisation
+
+ Returns:
+ The current instance of the ServiceManagerWidget
+ """
+
+
+ # Open transport
+ await self._service_manager._transport.open()
+
+ self.loading = True
+ try:
+ # Handle authorization
+ if isinstance(self._authorizer, AuthorizationCodeAuthorizer):
+ if not await self._authorizer.reuse_token():
+ await self._authorizer.login(timeout_seconds=timeout_seconds)
+
+ # Refresh services after login
+ await self.refresh_services()
+
+ finally:
+ self.loading = False
+
+ return self
+
+ @property
+ def cache(self):
+ """Get the cache instance used by this widget.
+
+ Returns:
+ The cache instance
+ """
+ return self._cache
+
+ async def refresh_services(self):
+ """Refresh the list of organizations, hubs, and workspaces."""
+
+ self.loading = True
+ try:
+ try:
+ await self._service_manager.refresh_organizations()
+ except UnauthorizedException:
+ # Re-login if token expired
+ await self.login()
+ await self._service_manager.refresh_organizations()
+
+ # Update organizations list
+ orgs = self._service_manager.list_organizations()
+ self.organizations = [
+ {"id": str(org.id), "name": org.display_name}
+ for org in orgs
+ ]
+
+ # Update signed in state and button
+ self.signed_in = True
+ self.button_text = "Refresh Evo Services"
+
+ # Refresh hubs and workspaces if already selected
+ if self.selected_org_id:
+ self._refresh_hubs()
+ if self.selected_hub_code:
+ await self._refresh_workspaces()
+
+ finally:
+ self.loading = False
+
+ def _refresh_hubs(self):
+ """Refresh the list of hubs for the selected organization."""
+ if self.selected_org_id:
+ try:
+ hub_list = self._service_manager.list_hubs()
+ self.hubs = [
+ {"code": hub.code, "name": hub.display_name}
+ for hub in hub_list
+ ]
+ except Exception:
+ self.hubs = []
+ else:
+ self.hubs = []
+
+ async def _refresh_workspaces(self):
+ """Refresh the list of workspaces for the selected hub."""
+ if self.selected_hub_code:
+ try:
+ await self._service_manager.refresh_workspaces()
+ ws_list = self._service_manager.list_workspaces()
+ self.workspaces = [
+ {"id": str(ws.id), "name": ws.display_name}
+ for ws in ws_list
+ ]
+ except Exception:
+ self.workspaces = []
+ else:
+ self.workspaces = []
+
+ def _on_action(self, change):
+ """Handle action messages from the frontend."""
+ action = change['new']
+ if action == 'refresh':
+ asyncio.create_task(self.refresh_services())
+
+ def _on_org_change(self, change):
+ """Handle organization selection changes."""
+ org_id = change['new']
+ if org_id and self._service_manager:
+ try:
+ uuid_org_id = UUID(org_id) if org_id else None
+ self._service_manager.set_current_organization(uuid_org_id)
+ self._refresh_hubs()
+ except Exception:
+ pass
+
+ def _on_hub_change(self, change):
+ """Handle hub selection changes."""
+ hub_code = change['new']
+ if self._service_manager:
+ self._service_manager.set_current_hub(hub_code if hub_code else None)
+ asyncio.create_task(self._refresh_workspaces())
+
+ def _on_workspace_change(self, change):
+ """Handle workspace selection changes."""
+ workspace_id = change['new']
+ if workspace_id and self._service_manager:
+ try:
+ uuid_ws_id = UUID(workspace_id) if workspace_id else None
+ self._service_manager.set_current_workspace(uuid_ws_id)
+ except Exception:
+ pass
+
+ def get_connector(self):
+ """Get an API connector for the currently selected hub."""
+ return self._service_manager.get_connector()
+
+ def get_environment(self):
+ """Get an environment with the currently selected organization, hub, and workspace."""
+ return self._service_manager.get_environment()
+
+ def create_client(self, client_class, *args, **kwargs):
+ """Create a client for the currently selected workspace."""
+ return self._service_manager.create_client(client_class, *args, **kwargs)
+
+
+
+class FeedbackWidget(anywidget.AnyWidget):
+ """Simple feedback widget for displaying progress and messages to the user."""
+
+ _esm = Path(__file__).parent / "static" / "feedback.js"
+ _css = Path(__file__).parent / "static" / "feedback.css"
+
+ # Widget state
+ label = traitlets.Unicode("").tag(sync=True)
+ progress_value = traitlets.Float(0.0).tag(sync=True)
+ progress_percent = traitlets.Unicode("0.0%").tag(sync=True)
+ message = traitlets.Unicode("").tag(sync=True)
+
+ def __init__(self, label: str, **kwargs):
+ """Initialize the FeedbackWidget.
+
+ Args:
+ label: The label for the feedback widget
+ """
+ super().__init__(**kwargs)
+ self.label = label
+ self._last_message = ""
+
+ def progress(self, progress: float, message: str | None = None) -> None:
+ """Update the progress and optional message.
+
+ This can raise an exception to cancel the current operation.
+
+ Args:
+ progress: A float between 0 and 1 representing the progress (0-100%)
+ message: An optional message to display to the user
+ """
+ # Clamp progress between 0 and 1
+ progress = max(0.0, min(1.0, progress))
+
+ self.progress_value = progress
+ self.progress_percent = f"{progress * 100:5.1f}%"
+
+ if message is not None:
+ self.message = message
+ self._last_message = message
diff --git a/packages/evo-sdk-common/src/evo/notebooks/assets/__init__.py b/packages/evo-widgets/src/evo/widgets/static/__init__.py
similarity index 87%
rename from packages/evo-sdk-common/src/evo/notebooks/assets/__init__.py
rename to packages/evo-widgets/src/evo/widgets/static/__init__.py
index ec985302..445cffea 100644
--- a/packages/evo-sdk-common/src/evo/notebooks/assets/__init__.py
+++ b/packages/evo-widgets/src/evo/widgets/static/__init__.py
@@ -12,14 +12,14 @@
from importlib.abc import Traversable
from importlib.resources import files
-_ROOT = files(__name__)
+_IMAGES = files(__name__) / "images"
def get(filename: str) -> Traversable:
- """Get the path to a file in this directory.
+ """Get the path to a file in the images directory.
:param filename: The name of the file.
:return: A Traversable object representing the file.
"""
- return _ROOT / filename
+ return _IMAGES / filename
diff --git a/packages/evo-widgets/src/evo/widgets/static/feedback.css b/packages/evo-widgets/src/evo/widgets/static/feedback.css
new file mode 100644
index 00000000..32fe2a2e
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/static/feedback.css
@@ -0,0 +1,83 @@
+:root {
+ --evo-primary: #265C7F;
+ --evo-primary-hover: #1e4a63;
+ --evo-primary-light: #3a7ba3;
+ --evo-secondary: #2C3E50;
+ --evo-accent: #00A9E0;
+ --evo-success: #10b981;
+ --evo-success-dark: #047857;
+ --evo-error: #dc2626;
+ --evo-error-light: #f87171;
+ --evo-error-bg: #fef2f2;
+ --evo-error-bg-dark: #2d1616;
+ --evo-text-primary: #333;
+ --evo-text-secondary: #555;
+ --evo-text-tertiary: #666;
+ --evo-text-muted: #9ca3af;
+ --evo-text-dark: #6b7280;
+ --evo-border: #ccc;
+ --evo-border-light: #e1e4e8;
+ --evo-bg-disabled: #f5f5f5;
+ --evo-bg-hover: #f6f8fa;
+ --evo-bg-progress: #f0f0f0;
+ --evo-bg-highlight: #e3f2fd;
+}
+.feedback-container {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 5px;
+}
+
+.feedback-container,
+.feedback-label,
+.progress-text,
+.feedback-message {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+.feedback-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--evo-text-primary);
+ white-space: nowrap;
+}
+
+.progress-container {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.progress-bar {
+ width: 400px;
+ height: 24px;
+ background-color: var(--evo-bg-progress);
+ border: 1px solid var(--evo-border);
+ border-radius: 2px;
+ overflow: hidden;
+ position: relative;
+}
+
+.progress-fill {
+ height: 100%;
+ background-color: var(--evo-primary);
+ transition: width 0.3s ease;
+ border-radius:2px;
+}
+
+.progress-text {
+ min-width: 55px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--evo-text-secondary);
+ text-align: right;
+}
+
+.feedback-message {
+ font-size: 13px;
+ font-style: italic;
+ color: var(--evo-text-tertiary);
+ flex: 1;
+}
diff --git a/packages/evo-widgets/src/evo/widgets/static/feedback.js b/packages/evo-widgets/src/evo/widgets/static/feedback.js
new file mode 100644
index 00000000..cd502997
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/static/feedback.js
@@ -0,0 +1,73 @@
+function render({ model, el }) {
+ // Create main container
+ const container = document.createElement("div");
+ container.className = "feedback-container";
+
+ // Create label
+ const labelEl = document.createElement("div");
+ labelEl.className = "feedback-label";
+ labelEl.textContent = model.get("label");
+
+ // Create progress bar container
+ const progressContainer = document.createElement("div");
+ progressContainer.className = "progress-container";
+
+ // Create progress bar background
+ const progressBar = document.createElement("div");
+ progressBar.className = "progress-bar";
+
+ // Create progress bar fill
+ const progressFill = document.createElement("div");
+ progressFill.className = "progress-fill";
+
+ // Create progress percentage text
+ const progressText = document.createElement("div");
+ progressText.className = "progress-text";
+ progressText.textContent = model.get("progress_percent");
+
+ progressBar.appendChild(progressFill);
+ progressContainer.appendChild(progressBar);
+ progressContainer.appendChild(progressText);
+
+ // Create message label
+ const messageEl = document.createElement("div");
+ messageEl.className = "feedback-message";
+ messageEl.textContent = model.get("message");
+
+ // Assemble the widget
+ container.appendChild(labelEl);
+ container.appendChild(progressContainer);
+ container.appendChild(messageEl);
+
+ el.appendChild(container);
+
+ // Update progress bar
+ const updateProgress = () => {
+ const progress = model.get("progress_value");
+ const percent = model.get("progress_percent");
+ progressFill.style.width = `${progress * 100}%`;
+ progressText.textContent = percent;
+ };
+
+ // Update message
+ const updateMessage = () => {
+ messageEl.textContent = model.get("message");
+ };
+
+ // Update label
+ const updateLabel = () => {
+ labelEl.textContent = model.get("label");
+ };
+
+ // Initialize
+ updateProgress();
+ updateMessage();
+
+ // Listen to model changes
+ model.on("change:progress_value", updateProgress);
+ model.on("change:progress_percent", updateProgress);
+ model.on("change:message", updateMessage);
+ model.on("change:label", updateLabel);
+}
+
+export default { render };
diff --git a/packages/evo-widgets/src/evo/widgets/static/images/__init__.py b/packages/evo-widgets/src/evo/widgets/static/images/__init__.py
new file mode 100644
index 00000000..a11b4fef
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/static/images/__init__.py
@@ -0,0 +1 @@
+# Images directory
diff --git a/packages/evo-widgets/src/evo/widgets/static/service_manager.css b/packages/evo-widgets/src/evo/widgets/static/service_manager.css
new file mode 100644
index 00000000..1c14df5c
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/static/service_manager.css
@@ -0,0 +1,175 @@
+/* Service Manager Widget Styles */
+
+/* Seequent EVO Theme - Inline for anywidget compatibility */
+:root {
+ --evo-primary: #265C7F;
+ --evo-primary-hover: #1e4a63;
+ --evo-primary-light: #3a7ba3;
+ --evo-secondary: #2C3E50;
+ --evo-accent: #00A9E0;
+ --evo-success: #10b981;
+ --evo-success-dark: #047857;
+ --evo-error: #dc2626;
+ --evo-error-light: #f87171;
+ --evo-error-bg: #fef2f2;
+ --evo-error-bg-dark: #2d1616;
+ --evo-text-primary: #333;
+ --evo-text-secondary: #555;
+ --evo-text-tertiary: #666;
+ --evo-text-muted: #9ca3af;
+ --evo-text-dark: #6b7280;
+ --evo-border: #ccc;
+ --evo-border-light: #e1e4e8;
+ --evo-bg-disabled: #f5f5f5;
+ --evo-bg-hover: #f6f8fa;
+ --evo-bg-progress: #f0f0f0;
+ --evo-bg-highlight: #e3f2fd;
+}
+
+.evo-button {
+ background-color: var(--evo-primary);
+ color: white;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 0;
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.evo-button:hover:not(:disabled) {
+ background-color: var(--evo-primary-hover);
+}
+
+.evo-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.evo-spinner {
+ width: 25px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ border: 4px solid var(--evo-primary);
+ animation:
+ l20-1 0.8s infinite linear alternate,
+ l20-2 1.6s infinite linear;
+}
+
+@keyframes l20-1 {
+ 0% {clip-path: polygon(50% 50%,0 0, 50% 0%, 50% 0%, 50% 0%, 50% 0%, 50% 0% )}
+ 12.5% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 0%, 100% 0%, 100% 0% )}
+ 25% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 100% 100%, 100% 100% )}
+ 50% {clip-path: polygon(50% 50%,0 0, 50% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )}
+ 62.5% {clip-path: polygon(50% 50%,100% 0, 100% 0%, 100% 0%, 100% 100%, 50% 100%, 0% 100% )}
+ 75% {clip-path: polygon(50% 50%,100% 100%, 100% 100%, 100% 100%, 100% 100%, 50% 100%, 0% 100% )}
+ 100% {clip-path: polygon(50% 50%,50% 100%, 50% 100%, 50% 100%, 50% 100%, 50% 100%, 0% 100% )}
+}
+
+@keyframes l20-2 {
+ 0% {transform:scaleY(1) rotate(0deg)}
+ 49.99%{transform:scaleY(1) rotate(135deg)}
+ 50% {transform:scaleY(-1) rotate(0deg)}
+ 100% {transform:scaleY(-1) rotate(-135deg)}
+}
+
+.evo-dropdown {
+ padding: 6px 10px;
+ border: 1px solid var(--evo-border);
+ border-radius: 1x;
+ font-size: 13px;
+ background-color: white;
+ cursor: pointer;
+}
+
+.evo-dropdown:disabled {
+ background-color: var(--evo-bg-disabled);
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.evo-dropdown:focus {
+ outline: none;
+ border-color: var(--evo-primary);
+ box-shadow: 0 0 0 2px rgba(38, 92, 127, 0.1);
+}
+
+.evo-label {
+ font-weight: 500;
+ font-size: 13px;
+ color: var(--evo-text-primary);
+}
+
+/* Service Manager Specific Styles */
+
+.service-manager-container {
+ padding: 10px;
+}
+
+.service-manager-container,
+.sign-in-button,
+.selector-label,
+.selector-dropdown {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+.main-layout {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 20px;
+}
+
+.left-column {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ min-width: 400px;
+}
+
+.right-column {
+ flex: 1;
+}
+
+.header-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.evo-logo {
+ padding: 5px;
+ border-radius: 4px;
+}
+
+.sign-in-button {
+ margin: 5px;
+}
+
+.loading-spinner {
+ margin: 3px;
+ align-self: center;
+}
+
+.selector-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 5px 5px;
+}
+
+.selector-label {
+ min-width: 90px;
+}
+
+.selector-dropdown {
+ flex: 1;
+ min-width: 200px;
+}
+
+.selector-dropdown option {
+ padding: 5px;
+}
diff --git a/packages/evo-widgets/src/evo/widgets/static/service_manager.js b/packages/evo-widgets/src/evo/widgets/static/service_manager.js
new file mode 100644
index 00000000..348f7e81
--- /dev/null
+++ b/packages/evo-widgets/src/evo/widgets/static/service_manager.js
@@ -0,0 +1,189 @@
+function render({ model, el }) {
+ // Create main container
+ const container = document.createElement("div");
+ container.className = "service-manager-container";
+
+ // Create main layout (left column and right column)
+ const mainLayout = document.createElement("div");
+ mainLayout.className = "main-layout";
+
+ // Left column - controls
+ const leftColumn = document.createElement("div");
+ leftColumn.className = "left-column";
+
+ // Header row with logo, button, and loading indicator
+ const headerRow = document.createElement("div");
+ headerRow.className = "header-row";
+
+ // Logo
+ const logo = document.createElement("div");
+ logo.className = "evo-logo";
+ logo.textContent = "EVO";
+ headerRow.appendChild(logo);
+
+ // Sign in button
+ const signInBtn = document.createElement("button");
+ signInBtn.className = "sign-in-button evo-button";
+ signInBtn.textContent = model.get("button_text");
+ signInBtn.onclick = () => {
+ model.set("action", "refresh");
+ model.save_changes();
+ };
+ headerRow.appendChild(signInBtn);
+
+ // Loading spinner
+ const loadingSpinner = document.createElement("div");
+ loadingSpinner.className = "loading-spinner evo-spinner";
+ loadingSpinner.style.display = model.get("loading") ? "block" : "none";
+ headerRow.appendChild(loadingSpinner);
+
+ leftColumn.appendChild(headerRow);
+
+ // Organization selector
+ const orgRow = document.createElement("div");
+ orgRow.className = "selector-row";
+
+ const orgLabel = document.createElement("label");
+ orgLabel.textContent = "Organisation:";
+ orgLabel.className = "selector-label evo-label";
+
+ const orgSelect = document.createElement("select");
+ orgSelect.className = "selector-dropdown evo-dropdown";
+ orgSelect.innerHTML = '';
+
+ const updateOrgs = () => {
+ const orgs = model.get("organizations");
+ const selectedId = model.get("selected_org_id");
+ orgSelect.innerHTML = '';
+ orgs.forEach(org => {
+ const option = document.createElement("option");
+ option.value = org.id;
+ option.textContent = org.name;
+ if (org.id === selectedId) {
+ option.selected = true;
+ }
+ orgSelect.appendChild(option);
+ });
+ orgSelect.disabled = orgs.length === 0;
+ };
+
+ orgSelect.onchange = () => {
+ model.set("selected_org_id", orgSelect.value);
+ model.save_changes();
+ };
+
+ orgRow.appendChild(orgLabel);
+ orgRow.appendChild(orgSelect);
+ leftColumn.appendChild(orgRow);
+
+ // Hub selector
+ const hubRow = document.createElement("div");
+ hubRow.className = "selector-row";
+
+ const hubLabel = document.createElement("label");
+ hubLabel.textContent = "Hub:";
+ hubLabel.className = "selector-label evo-label";
+
+ const hubSelect = document.createElement("select");
+ hubSelect.className = "selector-dropdown evo-dropdown";
+ hubSelect.innerHTML = '';
+
+ const updateHubs = () => {
+ const hubs = model.get("hubs");
+ const selectedCode = model.get("selected_hub_code");
+ hubSelect.innerHTML = '';
+ hubs.forEach(hub => {
+ const option = document.createElement("option");
+ option.value = hub.code;
+ option.textContent = hub.name;
+ if (hub.code === selectedCode) {
+ option.selected = true;
+ }
+ hubSelect.appendChild(option);
+ });
+ hubSelect.disabled = hubs.length === 0;
+ };
+
+ hubSelect.onchange = () => {
+ model.set("selected_hub_code", hubSelect.value);
+ model.save_changes();
+ };
+
+ hubRow.appendChild(hubLabel);
+ hubRow.appendChild(hubSelect);
+ leftColumn.appendChild(hubRow);
+
+ // Workspace selector
+ const workspaceRow = document.createElement("div");
+ workspaceRow.className = "selector-row";
+
+ const workspaceLabel = document.createElement("label");
+ workspaceLabel.textContent = "Workspace:";
+ workspaceLabel.className = "selector-label evo-label";
+
+ const workspaceSelect = document.createElement("select");
+ workspaceSelect.className = "selector-dropdown evo-dropdown";
+ workspaceSelect.innerHTML = '';
+
+ const updateWorkspaces = () => {
+ const workspaces = model.get("workspaces");
+ const selectedId = model.get("selected_workspace_id");
+ workspaceSelect.innerHTML = '';
+ workspaces.forEach(ws => {
+ const option = document.createElement("option");
+ option.value = ws.id;
+ option.textContent = ws.name;
+ if (ws.id === selectedId) {
+ option.selected = true;
+ }
+ workspaceSelect.appendChild(option);
+ });
+ workspaceSelect.disabled = workspaces.length === 0;
+ };
+
+ workspaceSelect.onchange = () => {
+ model.set("selected_workspace_id", workspaceSelect.value);
+ model.save_changes();
+ };
+
+ workspaceRow.appendChild(workspaceLabel);
+ workspaceRow.appendChild(workspaceSelect);
+ leftColumn.appendChild(workspaceRow);
+
+ // Right column (for future prompt area)
+ const rightColumn = document.createElement("div");
+ rightColumn.className = "right-column";
+
+ mainLayout.appendChild(leftColumn);
+ mainLayout.appendChild(rightColumn);
+ container.appendChild(mainLayout);
+
+ // Add to DOM
+ el.appendChild(container);
+
+ // Initialize
+ updateOrgs();
+ updateHubs();
+ updateWorkspaces();
+
+ // Listen to model changes
+ model.on("change:organizations", updateOrgs);
+ model.on("change:hubs", updateHubs);
+ model.on("change:workspaces", updateWorkspaces);
+
+ model.on("change:button_text", () => {
+ signInBtn.textContent = model.get("button_text");
+ });
+
+ model.on("change:loading", () => {
+ const loading = model.get("loading");
+ loadingSpinner.style.display = loading ? "block" : "none";
+ signInBtn.disabled = loading;
+ });
+
+ model.on("change:selected_org_id", updateOrgs);
+ model.on("change:selected_hub_code", updateHubs);
+ model.on("change:selected_workspace_id", updateWorkspaces);
+}
+
+export default { render };
diff --git a/pyproject.toml b/pyproject.toml
index c8fb7df0..79636a7e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,6 +10,7 @@ dependencies = [
"evo-files[aiohttp,notebooks]",
"evo-colormaps[aiohttp,notebooks]",
"evo-compute[aiohttp,notebooks]",
+ "evo-widgets[aiohttp, notebooks]",
"jupyter",
]
dynamic = ["readme"]
@@ -53,6 +54,7 @@ evo-colormaps = { workspace = true }
evo-files = { workspace = true }
evo-objects = { workspace = true }
evo-compute = { workspace = true }
+evo-widgets = { workspace = true }
samples = { path = "samples" }
evo-blockmodels = { workspace = true }
diff --git a/samples/auth-and-evo-discovery/native-app-token.ipynb b/samples/auth-and-evo-discovery/native-app-token.ipynb
index bde4913c..317fac85 100644
--- a/samples/auth-and-evo-discovery/native-app-token.ipynb
+++ b/samples/auth-and-evo-discovery/native-app-token.ipynb
@@ -33,7 +33,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"cache\"\n",
"\n",
@@ -41,7 +41,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/blockmodels/api-examples.ipynb b/samples/blockmodels/api-examples.ipynb
index 41f007d4..c3d04f43 100644
--- a/samples/blockmodels/api-examples.ipynb
+++ b/samples/blockmodels/api-examples.ipynb
@@ -33,7 +33,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"./notebook-data\"\n",
"\n",
@@ -46,7 +46,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/blockmodels/sdk-examples.ipynb b/samples/blockmodels/sdk-examples.ipynb
index f645f4e2..d37a281f 100644
--- a/samples/blockmodels/sdk-examples.ipynb
+++ b/samples/blockmodels/sdk-examples.ipynb
@@ -35,7 +35,7 @@
"from datetime import datetime\n",
"\n",
"from evo.blockmodels import BlockModelAPIClient\n",
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"./notebook-data\"\n",
"input_path = f\"{cache_location}/input\"\n",
@@ -49,7 +49,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/colormaps/api-examples.ipynb b/samples/colormaps/api-examples.ipynb
index 5956dd15..e12edc45 100644
--- a/samples/colormaps/api-examples.ipynb
+++ b/samples/colormaps/api-examples.ipynb
@@ -34,7 +34,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"./notebook-data\"\n",
"\n",
@@ -47,7 +47,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/colormaps/sdk-examples.ipynb b/samples/colormaps/sdk-examples.ipynb
index 477d667d..ee81593a 100644
--- a/samples/colormaps/sdk-examples.ipynb
+++ b/samples/colormaps/sdk-examples.ipynb
@@ -29,7 +29,7 @@
"outputs": [],
"source": [
"from evo.colormaps import ColormapAPIClient\n",
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"from evo.objects import ObjectAPIClient\n",
"\n",
"cache_location = \"./notebook-data\"\n",
@@ -43,7 +43,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/geoscience-objects/download-drilling-campaign/download-drilling-campaign.ipynb b/samples/geoscience-objects/download-drilling-campaign/download-drilling-campaign.ipynb
index cf95c0a7..19e651dc 100644
--- a/samples/geoscience-objects/download-drilling-campaign/download-drilling-campaign.ipynb
+++ b/samples/geoscience-objects/download-drilling-campaign/download-drilling-campaign.ipynb
@@ -32,7 +32,7 @@
"\n",
"import pandas as pd\n",
"\n",
- "from evo.notebooks import FeedbackWidget, ServiceManagerWidget\n",
+ "from evo.widgets import FeedbackWidget, ServiceManagerWidget\n",
"from evo.objects import ObjectAPIClient\n",
"\n",
"cache_location = \"data\"\n",
@@ -46,7 +46,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/geoscience-objects/download-pointset/download-pointset.ipynb b/samples/geoscience-objects/download-pointset/download-pointset.ipynb
index df6bceec..c9678f3e 100644
--- a/samples/geoscience-objects/download-pointset/download-pointset.ipynb
+++ b/samples/geoscience-objects/download-pointset/download-pointset.ipynb
@@ -28,7 +28,7 @@
"source": [
"import pandas as pd\n",
"\n",
- "from evo.notebooks import FeedbackWidget, ServiceManagerWidget\n",
+ "from evo.widgets import FeedbackWidget, ServiceManagerWidget\n",
"from evo.objects import ObjectAPIClient\n",
"\n",
"cache_location = \"data\"\n",
@@ -42,7 +42,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
diff --git a/samples/geoscience-objects/publish-downhole-collection/publish-downhole-collection.ipynb b/samples/geoscience-objects/publish-downhole-collection/publish-downhole-collection.ipynb
index 36c01eae..96584411 100644
--- a/samples/geoscience-objects/publish-downhole-collection/publish-downhole-collection.ipynb
+++ b/samples/geoscience-objects/publish-downhole-collection/publish-downhole-collection.ipynb
@@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"data\"\n",
"input_path = f\"{cache_location}/input\"\n",
@@ -40,7 +40,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -1202,7 +1203,7 @@
"source": [
"from evo_schemas.objects import DownholeCollection_V1_2_0\n",
"\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"# Lastly, assemble the complete geoscience object by combining all previously defined components.\n",
"# - The name and UUID are used to identify the object.\n",
diff --git a/samples/geoscience-objects/publish-drilling-campaign/publish-drilling-campaign.ipynb b/samples/geoscience-objects/publish-drilling-campaign/publish-drilling-campaign.ipynb
index 39de0521..c55dd954 100644
--- a/samples/geoscience-objects/publish-drilling-campaign/publish-drilling-campaign.ipynb
+++ b/samples/geoscience-objects/publish-drilling-campaign/publish-drilling-campaign.ipynb
@@ -28,7 +28,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"data\"\n",
"input_path = f\"{cache_location}/input\"\n",
@@ -42,7 +42,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -627,7 +628,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"await data_client.upload_referenced_data(dc.as_dict(), FeedbackWidget(\"Uploading data\"))\n",
"new_drilling_campaign_metadata = await object_client.create_geoscience_object(full_obj_path, dc.as_dict())"
diff --git a/samples/geoscience-objects/publish-pointset/publish-pointset.ipynb b/samples/geoscience-objects/publish-pointset/publish-pointset.ipynb
index dd897526..f4b02a84 100644
--- a/samples/geoscience-objects/publish-pointset/publish-pointset.ipynb
+++ b/samples/geoscience-objects/publish-pointset/publish-pointset.ipynb
@@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"data\"\n",
"input_path = f\"{cache_location}/input\"\n",
@@ -40,7 +40,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -333,7 +334,7 @@
"\n",
"from evo_schemas.objects import Pointset_V1_2_0\n",
"\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"pointset = Pointset_V1_2_0(\n",
" name=object_name,\n",
diff --git a/samples/geoscience-objects/publish-regular-2d-grid/publish-regular-2d-grid.ipynb b/samples/geoscience-objects/publish-regular-2d-grid/publish-regular-2d-grid.ipynb
index c8ba8a39..360b56fd 100644
--- a/samples/geoscience-objects/publish-regular-2d-grid/publish-regular-2d-grid.ipynb
+++ b/samples/geoscience-objects/publish-regular-2d-grid/publish-regular-2d-grid.ipynb
@@ -32,7 +32,7 @@
"import geosoft.gxpy.gx as gx\n",
"from geosoft.gxapi import GXAPIError\n",
"\n",
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"# Create a GX context\n",
"try:\n",
@@ -59,7 +59,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -458,7 +459,7 @@
"source": [
"from evo_schemas.objects import Regular2DGrid_V1_2_0\n",
"\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"# Assemble the complete geoscience object by combining all previously defined components.\n",
"# - The name and UUID are used to identify the object.\n",
diff --git a/samples/geoscience-objects/publish-triangular-mesh/publish-triangular-mesh.ipynb b/samples/geoscience-objects/publish-triangular-mesh/publish-triangular-mesh.ipynb
index d25eba0e..fbce4e16 100644
--- a/samples/geoscience-objects/publish-triangular-mesh/publish-triangular-mesh.ipynb
+++ b/samples/geoscience-objects/publish-triangular-mesh/publish-triangular-mesh.ipynb
@@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"\n",
"cache_location = \"data\"\n",
"input_path = f\"{cache_location}/input\"\n",
@@ -40,7 +40,8 @@
" redirect_url=redirect_url,\n",
" client_id=client_id,\n",
" cache_location=cache_location,\n",
- ").login()"
+ ")\n",
+ "manager"
]
},
{
@@ -271,7 +272,7 @@
"from evo_schemas.components import Triangles_V1_2_0\n",
"from evo_schemas.objects import TriangleMesh_V2_1_0\n",
"\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"# Lastly, assemble the complete geoscience object by combining all previously defined components.\n",
"# - The name and UUID are used to identify the object.\n",
diff --git a/samples/workspaces/bonus/move-objects.ipynb b/samples/workspaces/bonus/move-objects.ipynb
index 49649ccb..01e1431c 100644
--- a/samples/workspaces/bonus/move-objects.ipynb
+++ b/samples/workspaces/bonus/move-objects.ipynb
@@ -7,7 +7,7 @@
"metadata": {},
"outputs": [],
"source": [
- "from evo.notebooks import ServiceManagerWidget\n",
+ "from evo.widgets import ServiceManagerWidget\n",
"from evo.objects import ObjectAPIClient\n",
"\n",
"cache_location = \"data\"\n",
@@ -141,7 +141,7 @@
"\n",
"from workspace_utils import collect_data_references\n",
"\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"# Download the selected object\n",
"if selected_id:\n",
@@ -209,7 +209,7 @@
"from pathlib import Path\n",
"\n",
"from evo.common import Environment\n",
- "from evo.notebooks import FeedbackWidget\n",
+ "from evo.widgets import FeedbackWidget\n",
"\n",
"# Get the target workspace ID and operation\n",
"target_workspace_id = target_workspace_selector.get_selected_workspace_id()\n",
diff --git a/uv.lock b/uv.lock
index 36fe4fcc..78dc1f20 100644
--- a/uv.lock
+++ b/uv.lock
@@ -17,6 +17,7 @@ members = [
"evo-objects",
"evo-sdk",
"evo-sdk-common",
+ "evo-widgets",
]
[[package]]
@@ -185,6 +186,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
+[[package]]
+name = "anywidget"
+version = "0.9.21"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ipywidgets" },
+ { name = "psygnal" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/5e/cbea445bf062b81e4d366ca29dae4f0aedc7a64f384afc24670e07bec560/anywidget-0.9.21.tar.gz", hash = "sha256:b8d0172029ac426573053c416c6a587838661612208bb390fa0607862e594b27", size = 390517, upload-time = "2025-11-12T17:06:03.035Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/03/c17464bbf682ea87e7e3de2ddc63395e359a78ae9c01f55fc78759ecbd79/anywidget-0.9.21-py3-none-any.whl", hash = "sha256:78c268e0fbdb1dfd15da37fb578f9cf0a0df58a430e68d9156942b7a9391a761", size = 231797, upload-time = "2025-11-12T17:06:01.564Z" },
+]
+
[[package]]
name = "appnope"
version = "0.1.4"
@@ -871,7 +886,7 @@ test = [
[[package]]
name = "evo-compute"
-version = "0.0.1rc1"
+version = "0.0.1rc2"
source = { editable = "packages/evo-compute" }
dependencies = [
{ name = "evo-sdk-common" },
@@ -992,7 +1007,7 @@ test = [
[[package]]
name = "evo-objects"
-version = "0.3.2"
+version = "0.3.3"
source = { editable = "packages/evo-objects" }
dependencies = [
{ name = "evo-sdk-common", extra = ["jmespath"] },
@@ -1071,7 +1086,7 @@ test = [
[[package]]
name = "evo-sdk"
-version = "0.1.13"
+version = "0.1.15"
source = { editable = "." }
dependencies = [
{ name = "evo-blockmodels", extra = ["aiohttp", "notebooks", "pyarrow"] },
@@ -1080,6 +1095,7 @@ dependencies = [
{ name = "evo-files", extra = ["aiohttp", "notebooks"] },
{ name = "evo-objects", extra = ["aiohttp", "notebooks", "utils"] },
{ name = "evo-sdk-common", extra = ["aiohttp", "jmespath", "notebooks"] },
+ { name = "evo-widgets", extra = ["aiohttp", "notebooks"] },
{ name = "jupyter" },
]
@@ -1109,6 +1125,7 @@ requires-dist = [
{ name = "evo-files", extras = ["aiohttp", "notebooks"], editable = "packages/evo-files" },
{ name = "evo-objects", extras = ["aiohttp", "notebooks", "utils"], editable = "packages/evo-objects" },
{ name = "evo-sdk-common", extras = ["aiohttp", "notebooks", "jmespath"], editable = "packages/evo-sdk-common" },
+ { name = "evo-widgets", extras = ["aiohttp", "notebooks"], editable = "packages/evo-widgets" },
{ name = "jupyter" },
]
@@ -1132,7 +1149,7 @@ test = [
[[package]]
name = "evo-sdk-common"
-version = "0.5.8"
+version = "0.5.10"
source = { editable = "packages/evo-sdk-common" }
dependencies = [
{ name = "pure-interface" },
@@ -1216,6 +1233,34 @@ test = [
{ name = "pytest" },
]
+[[package]]
+name = "evo-widgets"
+version = "0.1.0"
+source = { editable = "packages/evo-widgets" }
+dependencies = [
+ { name = "anywidget" },
+ { name = "evo-sdk-common" },
+ { name = "traitlets" },
+]
+
+[package.optional-dependencies]
+aiohttp = [
+ { name = "evo-sdk-common", extra = ["aiohttp"] },
+]
+notebooks = [
+ { name = "evo-sdk-common", extra = ["notebooks"] },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "anywidget", specifier = ">=0.9.21" },
+ { name = "evo-sdk-common", editable = "packages/evo-sdk-common" },
+ { name = "evo-sdk-common", extras = ["aiohttp"], marker = "extra == 'aiohttp'", editable = "packages/evo-sdk-common" },
+ { name = "evo-sdk-common", extras = ["notebooks"], marker = "extra == 'notebooks'", editable = "packages/evo-sdk-common" },
+ { name = "traitlets", specifier = ">=5.14.3" },
+]
+provides-extras = ["aiohttp", "notebooks"]
+
[[package]]
name = "exceptiongroup"
version = "1.3.0"
@@ -2692,6 +2737,40 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" },
]
+[[package]]
+name = "psygnal"
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/20/70430999aa609adb0601ec0f72bd23790a6e51a80ae6e7dc6621e6c5ee2a/psygnal-0.15.0.tar.gz", hash = "sha256:5534f18e2d1536675e181c6f81cf04f4177b25a9e60fdcf724a25ce5cc195765", size = 124470, upload-time = "2025-10-15T12:05:50.522Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/91/a65b177c94269fb60eb913d0e8157498ee676901f054f0f04a7f0445b710/psygnal-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c33a022d2bdfa68c71f6fe964fb316b8cff36a936a6075bb14378823b5bd28d", size = 518166, upload-time = "2025-10-15T12:05:11.997Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b7/3ee2a09dd4cce366b6ba5870e5cd3e8563d428254e7371f45d4746bc5389/psygnal-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:27367d0b47866c6d9c47a19ae9c9570c1525f729314b1d864a7d6e052688645e", size = 576372, upload-time = "2025-10-15T12:05:13.91Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4c/42e597b47e64f4e87f5b70f03e027d0d535b1f302897d4409d774d6859fa/psygnal-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bafa232672ae1d0f51873629c38aeed85476b6620803e8daa14edf20716054c", size = 863424, upload-time = "2025-10-15T12:05:15.099Z" },
+ { url = "https://files.pythonhosted.org/packages/90/d3/dd08bf4dad38cd418865ed9b2785f640bec68f3e91d4903ac8dda5926408/psygnal-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:45234b9f6f6a793c3df2867f86c5b5223731eda7734768148175268042c6b7b8", size = 872568, upload-time = "2025-10-15T12:05:16.986Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/71/5daabc87e3962bfdc07e6a745aa513fe92779b18cb9c97517423d6dac241/psygnal-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fcade907d3385eb3bc97617f51f275dfc5db45f601cc8ef5c2d17b2f9db1d0d", size = 409544, upload-time = "2025-10-15T12:05:18.551Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/b7/1979a82f27c32e70b165b3f1282bbfbaf81a3e44ea85a4599487511533a7/psygnal-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d83239961c66f0763c26df121d8028eeb1cdebc3ce2d511836b3424dda591f3", size = 512136, upload-time = "2025-10-15T12:05:19.963Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/85/64e1b2cf86e563aca9498842b7a5fb3bbba38ed50d7306278417f687939e/psygnal-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:219550f78512cd274ee11966033843426a85ee333fbfed73d0f7ce1b153c547c", size = 568105, upload-time = "2025-10-15T12:05:22.015Z" },
+ { url = "https://files.pythonhosted.org/packages/17/44/744374443b6e30f2ede11eb182d698d97c0bd021d59e472a0f0a4ddccf8e/psygnal-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c29149a5042d79cb9dfb4d7b6b8c624296681b1533d58b7820c0817ffdd81c4", size = 854314, upload-time = "2025-10-15T12:05:23.489Z" },
+ { url = "https://files.pythonhosted.org/packages/94/56/782a5da7a3e0fa5019b617c47a963202de37dabb73f2e43b67b8d76bac0a/psygnal-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d4c9762102df30530044c5a44cc591240ff3b89bd67292e10c0b73cd694c84e9", size = 862143, upload-time = "2025-10-15T12:05:25.316Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/93/ee50e54c5a8693a6954647da7e2c6a3150c4a37f0760c6e87ac6de3037dc/psygnal-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f50938b3caf07e34ab044c19d4e9280a53ff65492c285ff211285f0a08934c1", size = 414136, upload-time = "2025-10-15T12:05:26.551Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/6d/f3adf8f66bf12651f35aff13dd4a6c88afffa815ef8b2b7fa60a602a6cd7/psygnal-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:82eb5767f6cba67fa2d034dab9ec94e8eaf465067666dea3e2f832f2c32debc3", size = 522774, upload-time = "2025-10-15T12:05:27.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/40/adc69bd677a2683f931614fdd716034ba5bc238752973bad3a1415b2f015/psygnal-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dbcc67b2282eebe2e4e55ff9b50dad6b811d4ab698c573a61a725a6296919ba", size = 576015, upload-time = "2025-10-15T12:05:29.423Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ce/ad35c19f489c563e6655a6ee9509e1af7ee864ae8fe95f04f851a47e141a/psygnal-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d65e2686c19997eb4495974abc972ca1661504e73b8b58b1fb8466baf0c7ae", size = 888755, upload-time = "2025-10-15T12:05:30.971Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/be/0f680df48bf819025ce4f486443471f541c1559e3ad474311f92fb9a8549/psygnal-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed3ff192cdd14956c2f7a0be4635fa72b2eb2773dfc58a6aa8c14926647041f2", size = 880071, upload-time = "2025-10-15T12:05:32.487Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/2d/c16b2e2a657a908d363ba4b1680cb827f152cb680c24a1add720c8bfde36/psygnal-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ed1fd5797df111c9f9b43a1dc01ffb7c76e19ddc9b0de969e0b816034345246", size = 417554, upload-time = "2025-10-15T12:05:33.758Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/b0/d4ef27d30e0336e5dd49a145bc5f55ad7e8c2d4403a8cc89827e3dc4e17d/psygnal-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eb11ecb42b4ff9e45d661396399029c41fbd1cfdd5dbd5c31a3f6f52c8fc2b90", size = 521990, upload-time = "2025-10-15T12:05:35.904Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1a/d78fcfa19c06d5ef610054e159ce2d08a0787af8e2ebdf425ba81284ce71/psygnal-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eace624bb6aa7ad42d1c047a2e3a531f68b3bfc63d8b4c3de9dec4cc122bb534", size = 574962, upload-time = "2025-10-15T12:05:37.175Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/7b/e9a6fa461ef266c5a23485004934b8f08a2a8ddc447802161ea56d9837dd/psygnal-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0172efeb861280bca05673989a4df21624f44344eff20b873d8c9d0edc01350", size = 884958, upload-time = "2025-10-15T12:05:38.789Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/a3/1c14461602090ae84120ebd4e47f46990c853e61a71716e69a1ce18c3909/psygnal-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c284edb17542dad0114ad2a942799d6526fa72be7d76d078a388469d584d034c", size = 876350, upload-time = "2025-10-15T12:05:40.013Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/71/d143b294259a9067cde1a1a5c4025e0a98dff876576a84495e50da7e1316/psygnal-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:c60d36d46c992835608030ff3fa918c06c7f22133391d90500585fef726f5d07", size = 417938, upload-time = "2025-10-15T12:05:41.302Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/0f/8f6e5339cdfe9c67b8a4250501b9b4ac488c836e56c9a15f65b4a3c7a1a8/psygnal-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f0125639597d42b8d78fcd61cc306d7ae71a198d8fac83ab64a07742e8bb1ca8", size = 521077, upload-time = "2025-10-15T12:05:42.491Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/46/7b93bad30b1df8ca4d5940b8b6ab60913ab26820f53066f37504f328b76b/psygnal-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d3e759e84c9396f4b1f30bf4b5efd83c5fd359745a72df44b639aa0e5e94c51d", size = 574562, upload-time = "2025-10-15T12:05:43.717Z" },
+ { url = "https://files.pythonhosted.org/packages/67/14/1c3b8bf8e341029856b9c09f3c115eb84dad1bf03e0fb849bee575cff8ed/psygnal-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:876e2f8b22236c0327e3da75a17e40a550d89efed904c1e9db23acdd4a66504d", size = 888609, upload-time = "2025-10-15T12:05:44.895Z" },
+ { url = "https://files.pythonhosted.org/packages/82/48/ff492974866f041debf57148f582c68247bec66cf0e354adef7db808cae3/psygnal-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5108268d08ac176ac6f8a0cad2c76883282d75a14663f806fdf207eb53e38014", size = 880256, upload-time = "2025-10-15T12:05:46.377Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/88/aafeeaf8543189e77dac5f833fe6fac1d3f37a62932da445ccd9533e6770/psygnal-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:6034cacebd252776743450be62f25df323f8cb4ed7b01a46fc4dcf540baa64a6", size = 422151, upload-time = "2025-10-15T12:05:47.972Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/68/ad28d0c0a089bcd813fc6355a448acf18c897b4ea02d33276b5f740c2a07/psygnal-0.15.0-py3-none-any.whl", hash = "sha256:023c361c38e8ada87d0704704e1f2b7e799e9771e00b8e174fb409ff9ddeb502", size = 91027, upload-time = "2025-10-15T12:05:49.179Z" },
+]
+
[[package]]
name = "ptyprocess"
version = "0.7.0"