Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions envs_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# SPDX-FileCopyrightText: 2022-present Spyder Development Team and envs-manager contributors
#
# SPDX-License-Identifier: MIT


def _jupyter_server_extension_points():
"""
Returns a list of dictionaries with metadata describing
where to find the `_load_jupyter_server_extension` function.
"""
from envs_manager.jupyter import EnvManagerApp

return [{"module": "envs_manager.jupyter", "app": EnvManagerApp}]
71 changes: 71 additions & 0 deletions envs_manager/backends/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

from __future__ import annotations

import logging
from pathlib import Path
import subprocess
from typing import TypedDict

import requests


logger = logging.getLogger("envs-manager")


PYPI_API_PACKAGE_INFO_URL = "https://pypi.org/pypi/{package_name}/json"
ANACONDA_API_PACKAGE_INFO = "https://api.anaconda.org/package/{channel}/{package_name}"

Expand Down Expand Up @@ -183,3 +188,69 @@ def list_packages(self) -> BackendActionResult:

def list_environments(self) -> BackendActionResult:
raise NotImplementedError

def create_kernelspec(
self,
name: str,
display_name: str | None = None,
profile: str | None = None,
prefix: str | None = None,
user: bool = True,
env: dict[str, str] | None = None,
frozen_modules: bool = False,
) -> BackendActionResult:
"""
Create a Jupyter kernelspec for the environment.

Parameters
----------
name : str
Name of the kernelspec.
display_name : str, optional
Display name of the kernelspec. If None, defaults to the environment name.
profile : str, optional
IPython profile to load. If None, defaults to the default profile.
prefix : str, optional
Install prefix for the kernelspec. If None, defaults to sys.prefix.
user : bool, optional
Install for the current user instead of system-wide. The default is True.
env : dict[str, str], optional
Environment variables to set for the kernel. The default is None.
frozen_modules : bool, optional
Enable frozen modules for potentially faster startup. The default is False.

Returns
-------
BackendActionResult
Result of the action.
"""
command = [self.python_executable_path, "-m", "ipykernel", "install"]
if user:
command.append("--user")
if name:
command.extend(["--name", name])
if display_name:
command.extend(["--display-name", display_name])
if profile:
command.extend(["--profile", profile])
if prefix:
command.extend(["--prefix", prefix])
if env:
for key, value in env.items():
command.extend(["--env", f"{key}={value}"])
if frozen_modules:
command.append("--frozen_modules")

try:
result = run_command(command, capture_output=True)
output = result.stdout or result.stderr
logger.info(output.strip())
except subprocess.CalledProcessError as error:
error_text = error.stderr.strip()
logger.error(error_text)
return BackendActionResult(status=False, output=error_text)
except Exception as error:
logger.error(error, exc_info=True)
return BackendActionResult(status=False, output=str(error))

return BackendActionResult(status=True, output=output.strip())
90 changes: 90 additions & 0 deletions envs_manager/jupyter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: 2022-present Spyder Development Team and envs-manager contributors
#
# SPDX-License-Identifier: MIT

from __future__ import annotations
import json
import typing as t

from traitlets import Unicode
from tornado import web
from jupyter_server.auth.decorator import authorized
from jupyter_server.extension.application import ExtensionApp
from jupyter_server.base.handlers import JupyterHandler

from envs_manager.manager import (
DEFAULT_BACKENDS_ROOT_PATH,
DEFAULT_BACKEND,
Manager,
ManagerActions,
)


class EnvManagerHandler(JupyterHandler):
"""Handler to list available environments."""

_handler_action_regex = (
rf"(?P<action>{'|'.join(action.value for action in ManagerActions)})"
)

auth_resource = "envs_manager"

def get_manager(self) -> Manager:
"""Get the environment manager instance."""
return Manager(
backend=self.get_argument("backend", None)
or self.settings["envs_manager_config"]["default_backend"],
root_path=self.settings["envs_manager_config"]["root_path"],
env_name=self.get_argument("env_name", None),
env_directory=self.get_argument("env_directory", None),
)

def get_options(self) -> dict[str, t.Any]:
return self.get_json_body() or {}

def write_json(self, data, status=200):
self.set_status(status)
self.set_header("Content-Type", "application/json")
self.finish(json.dumps(data))

@authorized
@web.authenticated
def post(self, action: str):
try:
manager = self.get_manager()
action_options = self.get_options()
self.write_json(
manager.run_action(ManagerActions(action), action_options),
status=200,
)
except Exception as e:
self.set_status(501)
self.finish(str(e))
self.log_exception(type(e), e, e.__traceback__)


class EnvManagerApp(ExtensionApp):
"""Jupyter extension for managing environments."""

name = "envs_manager"
extension_url = "/envs_manager"
open_browser = False

root_path = Unicode(
str(DEFAULT_BACKENDS_ROOT_PATH),
config=True,
help="Root path for the extension. Defaults to the Jupyter server root path.",
)

default_backend = Unicode(
DEFAULT_BACKEND,
config=True,
help="Default backend to use for managing environments.",
)

handlers = [
(
rf"{extension_url}/{EnvManagerHandler._handler_action_regex}",
EnvManagerHandler,
),
] # type: ignore[list-item]
6 changes: 4 additions & 2 deletions envs_manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
from pathlib import Path
from typing import TypedDict
from enum import Enum

from envs_manager.backends.api import BackendActionResult, BackendInstance
from envs_manager.backends.venv_interface import VEnvInterface
Expand All @@ -22,7 +23,7 @@
DEFAULT_ENVS_ROOT_PATH = DEFAULT_BACKENDS_ROOT_PATH / DEFAULT_BACKEND / "envs"


class ManagerActions:
class ManagerActions(Enum):
"""Enum with the possible actions that can be performed by the manager."""

CreateEnvironment = "create_environment"
Expand All @@ -36,6 +37,7 @@ class ManagerActions:
UpdatePackages = "update"
ListPackages = "list"
ListEnvironments = "list_environments"
CreateKernelSpec = "create_kernelspec"


class ManagerOptions(TypedDict):
Expand Down Expand Up @@ -118,7 +120,7 @@ def __init__(
)

def run_action(self, action: ManagerActions, action_options: dict | None = None):
method = getattr(self, action)
method = getattr(self, action.value)
if action_options is not None:
return method(**action_options)
else:
Expand Down
7 changes: 7 additions & 0 deletions jupyter-config/envs_manager.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ServerApp": {
"jpserver_extensions": {
"envs_manager": true
}
}
}
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ pre-commit = [
[tool.hatch.version]
path = "envs_manager/__about__.py"


[tool.hatch.build.targets.sdist]
[tool.hatch.build.targets.wheel]

[tool.hatch.build.targets.wheel.shared-data]
"jupyter-config" = "etc/jupyter/jupyter_server_config.d"

[tool.hatch.envs.default]
dependencies = [
"pytest",
Expand Down
Loading