uv-kernel-manager 0.1.1a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uv_kernel/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Manage Jupyter kernels for uv-managed projects."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ # The installed distribution metadata is generated from [project].version in
6
+ # pyproject.toml, keeping that file as the only version authority.
7
+ __version__ = version("uv-kernel-manager")
uv_kernel/cli.py ADDED
@@ -0,0 +1,146 @@
1
+ """Command line interface for uv-kernel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from . import __version__
12
+ from .core import (UVKernelError, display_name, doctor, ensure_ipykernel,
13
+ find_project, install, matching, owned_kernels, remove, stale)
14
+
15
+ DESCRIPTION = """
16
+ Manage Jupyter kernels for uv-managed Python environments.
17
+
18
+ Register a project's uv `.venv` with the Jupyter installation you already use.
19
+ Only kernels carrying uv-kernel's ownership metadata are ever changed or removed.
20
+ """
21
+
22
+
23
+ def parser() -> argparse.ArgumentParser:
24
+ p = argparse.ArgumentParser(prog="uv-kernel", description=DESCRIPTION,
25
+ formatter_class=argparse.RawDescriptionHelpFormatter,
26
+ epilog="Examples:\n uv-kernel register\n uv-kernel --json list\n uv-kernel clean --yes")
27
+ p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
28
+ p.add_argument("--json", action="store_true", dest="json_output", help="Output machine-readable JSON.")
29
+ sub = p.add_subparsers(dest="command", required=True, metavar="COMMAND")
30
+ register = sub.add_parser("register", help="Register the current uv project as a Jupyter kernel.",
31
+ description="Adds ipykernel as a development dependency by default and writes a standard user kernelspec.")
32
+ register.add_argument("--no-add-ipykernel", action="store_true", help="Do not run `uv add --dev ipykernel` first.")
33
+ naming = register.add_mutually_exclusive_group()
34
+ naming.add_argument("--name", metavar="NAME", help="Use NAME as the kernel label instead of the suggested label.")
35
+ naming.add_argument("--yes", action="store_true", help="Accept the suggested kernel label without prompting.")
36
+ sub.add_parser("unregister", help="Remove this project's uv-kernel-owned kernel.",
37
+ description="Removes only kernels marked as owned by uv-kernel for the current project.")
38
+ sub.add_parser("list", help="List every kernel owned by uv-kernel.")
39
+ sub.add_parser("status", help="Show this project's uv-kernel registration status.")
40
+ sub.add_parser("refresh", help="Recreate this project's owned kernel with current settings.",
41
+ description="Updates the spec for the current .venv; it never touches other kernels.")
42
+ clean = sub.add_parser("clean", help="Find stale uv-kernel-owned kernels safely.",
43
+ description="Reports kernels whose project or .venv interpreter is gone. Nothing is deleted without --yes.")
44
+ clean.add_argument("--yes", action="store_true", help="Remove the stale kernels reported by clean.")
45
+ sub.add_parser("doctor", help="Diagnose uv, the current project, and Jupyter kernel paths.")
46
+ return p
47
+
48
+
49
+ def emit(value: Any, as_json: bool) -> None:
50
+ if as_json:
51
+ print(json.dumps(value, indent=2, sort_keys=True))
52
+ return
53
+ if isinstance(value, list):
54
+ if not value:
55
+ print("No uv-kernel-owned kernels found.")
56
+ for item in value:
57
+ print(f"{item['name']}: {item.get('display_name', '')} — {item.get('project_path', '')}")
58
+ elif isinstance(value, dict):
59
+ for key, item in value.items(): print(f"{key}: {item}")
60
+ else: print(value)
61
+
62
+
63
+ def choose_kernel_name(default: str) -> str:
64
+ """Prompt for a kernel label, accepting the suggested label on Enter."""
65
+ try:
66
+ response = input(
67
+ f"Register kernel as: {default}\n"
68
+ "Press Enter to continue, or enter a custom name: "
69
+ ).strip()
70
+ except EOFError as exc:
71
+ raise UVKernelError("No kernel name was provided. Use --yes or --name for non-interactive use.") from exc
72
+ return response or default
73
+
74
+
75
+ def main(argv: list[str] | None = None) -> int:
76
+ args = parser().parse_args(argv)
77
+ try:
78
+ if args.command == "list":
79
+ emit(owned_kernels(), args.json_output)
80
+ return 0
81
+
82
+ if args.command == "clean":
83
+ items = stale(owned_kernels())
84
+ if args.yes:
85
+ removed = remove(items)
86
+ emit({"removed": removed, "count": len(removed)}, args.json_output)
87
+ else:
88
+ emit(items, args.json_output)
89
+ if not args.json_output and items:
90
+ print("Run `uv-kernel clean --yes` to remove these kernels.")
91
+ return 0
92
+
93
+ if args.command == "doctor":
94
+ try:
95
+ project = find_project()
96
+ except UVKernelError:
97
+ project = None
98
+ emit(doctor(project), args.json_output)
99
+ return 0
100
+
101
+ project = find_project()
102
+ if args.command == "register":
103
+ suggested_name = display_name(project)
104
+ if args.name:
105
+ kernel_display_name = args.name
106
+ elif args.yes:
107
+ kernel_display_name = suggested_name
108
+ elif args.json_output:
109
+ raise UVKernelError("Use --yes or --name with --json register.")
110
+ else:
111
+ kernel_display_name = choose_kernel_name(suggested_name)
112
+ if not args.no_add_ipykernel:
113
+ ensure_ipykernel(project)
114
+ result = install(project, kernel_display_name=kernel_display_name)
115
+ emit({"registered": result}, args.json_output)
116
+ return 0
117
+
118
+ if args.command == "unregister":
119
+ removed = remove(matching(project))
120
+ emit({"removed": removed, "count": len(removed)}, args.json_output)
121
+ return 0
122
+
123
+ if args.command == "status":
124
+ items = matching(project)
125
+ result = {"project": str(project.root), "registered": bool(items), "kernels": items}
126
+ emit(result, args.json_output)
127
+ return 0 if items else 1
128
+
129
+ if args.command == "refresh":
130
+ remove(matching(project))
131
+ result = install(project)
132
+ emit({"refreshed": result}, args.json_output)
133
+ return 0
134
+
135
+ except UVKernelError as exc:
136
+ if args.json_output:
137
+ print(json.dumps({"error": str(exc)}))
138
+ else:
139
+ print(f"uv-kernel: error: {exc}", file=sys.stderr)
140
+ return 2
141
+
142
+ return 0
143
+
144
+
145
+ if __name__ == "__main__":
146
+ raise SystemExit(main())
uv_kernel/core.py ADDED
@@ -0,0 +1,171 @@
1
+ """Project discovery and safe Jupyter kernelspec operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import tomllib
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from jupyter_client.kernelspec import KernelSpecManager
19
+
20
+ from . import __version__
21
+
22
+ OWNER_KEY = "uv-kernel"
23
+
24
+
25
+ class UVKernelError(RuntimeError):
26
+ """A recoverable command error which should be shown to the user."""
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Project:
31
+ root: Path
32
+ name: str
33
+ python: Path
34
+
35
+
36
+ def find_project(start: Path | None = None) -> Project:
37
+ """Find the closest enclosing uv project and its project-local interpreter."""
38
+ directory = (start or Path.cwd()).resolve()
39
+ for candidate in (directory, *directory.parents):
40
+ manifest = candidate / "pyproject.toml"
41
+ if manifest.is_file():
42
+ try:
43
+ data = tomllib.loads(manifest.read_text(encoding="utf-8"))
44
+ except tomllib.TOMLDecodeError as exc:
45
+ raise UVKernelError(f"Cannot read {manifest}: {exc}") from exc
46
+ name = data.get("project", {}).get("name") or candidate.name
47
+ if not isinstance(name, str) or not name.strip():
48
+ name = candidate.name
49
+ python = candidate / ".venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
50
+ return Project(candidate, name, python)
51
+ raise UVKernelError("No pyproject.toml found in this directory or its parents.")
52
+
53
+
54
+ def kernel_id(project: Project) -> str:
55
+ """A stable, collision-resistant Jupyter kernelspec identifier."""
56
+ slug = re.sub(r"[^a-z0-9]+", "-", project.name.lower()).strip("-") or "project"
57
+ digest = hashlib.sha256(str(project.root).encode()).hexdigest()[:10]
58
+ return f"uv-kernel-{slug}-{digest}"
59
+
60
+
61
+ def python_version(python: Path) -> str:
62
+ try:
63
+ result = subprocess.run(
64
+ [str(python), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
65
+ capture_output=True, text=True, check=True,
66
+ )
67
+ except (OSError, subprocess.CalledProcessError) as exc:
68
+ raise UVKernelError(f"Could not run project Python at {python}: {exc}") from exc
69
+ return result.stdout.strip()
70
+
71
+
72
+ def display_name(project: Project) -> str:
73
+ return f"Python {python_version(project.python)} ({project.name})"
74
+
75
+
76
+ def ensure_ipykernel(project: Project) -> None:
77
+ """Ask uv to add ipykernel as a development dependency."""
78
+ try:
79
+ subprocess.run(["uv", "add", "--dev", "ipykernel"], cwd=project.root, check=True)
80
+ except FileNotFoundError as exc:
81
+ raise UVKernelError("uv was not found on PATH; install uv before registering.") from exc
82
+ except subprocess.CalledProcessError as exc:
83
+ raise UVKernelError("uv could not add the development dependency ipykernel.") from exc
84
+
85
+
86
+ def spec_data(project: Project, kernel_display_name: str | None = None) -> dict[str, Any]:
87
+ return {
88
+ "argv": [str(project.python), "-m", "ipykernel_launcher", "-f", "{connection_file}"],
89
+ "display_name": kernel_display_name or display_name(project),
90
+ "language": "python",
91
+ "metadata": {OWNER_KEY: {
92
+ "version": __version__, "project_path": str(project.root),
93
+ "project_name": project.name,
94
+ }},
95
+ }
96
+
97
+
98
+ def owned(spec: dict[str, Any]) -> bool:
99
+ return isinstance(spec.get("spec", {}).get("metadata", {}).get(OWNER_KEY), dict)
100
+
101
+
102
+ def owned_kernels(manager: KernelSpecManager | None = None) -> list[dict[str, Any]]:
103
+ manager = manager or KernelSpecManager()
104
+ results = []
105
+ for name, data in manager.get_all_specs().items():
106
+ if owned(data):
107
+ meta = data["spec"]["metadata"][OWNER_KEY]
108
+ results.append({
109
+ "name": name, "display_name": data["spec"].get("display_name"),
110
+ "resource_dir": data.get("resource_dir"), "project_path": meta.get("project_path"),
111
+ "project_name": meta.get("project_name"), "version": meta.get("version"),
112
+ })
113
+ return sorted(results, key=lambda item: item["name"])
114
+
115
+
116
+ def install(
117
+ project: Project,
118
+ manager: KernelSpecManager | None = None,
119
+ kernel_display_name: str | None = None,
120
+ ) -> dict[str, Any]:
121
+ if not project.python.is_file():
122
+ raise UVKernelError(f"No project interpreter found at {project.python}. Run `uv sync` first.")
123
+ manager = manager or KernelSpecManager()
124
+ name = kernel_id(project)
125
+ with tempfile.TemporaryDirectory(prefix="uv-kernel-") as temp:
126
+ Path(temp, "kernel.json").write_text(
127
+ json.dumps(spec_data(project, kernel_display_name), indent=2) + "\n",
128
+ encoding="utf-8",
129
+ )
130
+ manager.install_kernel_spec(temp, kernel_name=name, user=True, replace=True)
131
+ return {
132
+ "name": name,
133
+ "display_name": kernel_display_name or display_name(project),
134
+ "project_path": str(project.root),
135
+ }
136
+
137
+
138
+ def matching(project: Project, manager: KernelSpecManager | None = None) -> list[dict[str, Any]]:
139
+ return [item for item in owned_kernels(manager) if item["project_path"] == str(project.root)]
140
+
141
+
142
+ def remove(items: list[dict[str, Any]], manager: KernelSpecManager | None = None) -> list[str]:
143
+ manager = manager or KernelSpecManager()
144
+ removed: list[str] = []
145
+ for item in items:
146
+ manager.remove_kernel_spec(item["name"])
147
+ removed.append(item["name"])
148
+ return removed
149
+
150
+
151
+ def stale(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
152
+ return [item for item in items if not item.get("project_path") or not Path(item["project_path"]).is_dir()
153
+ or not Path(item["project_path"], ".venv", "Scripts/python.exe" if os.name == "nt" else "bin/python").is_file()]
154
+
155
+
156
+ def doctor(project: Project | None) -> dict[str, Any]:
157
+ details: dict[str, Any] = {
158
+ "uv": shutil.which("uv"), "jupyter_paths": KernelSpecManager().kernel_dirs,
159
+ "python": sys.executable,
160
+ }
161
+ if project:
162
+ details["project"] = {
163
+ "path": str(project.root), "name": project.name,
164
+ "interpreter": str(project.python), "interpreter_exists": project.python.is_file()
165
+ }
166
+ if project.python.is_file():
167
+ try:
168
+ details["project"]["python_version"] = python_version(project.python)
169
+ except UVKernelError as exc:
170
+ details["project"]["python_error"] = str(exc)
171
+ return details
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.5
2
+ Name: uv-kernel-manager
3
+ Version: 0.1.1a1
4
+ Summary: A lightweight CLI to seamlessly register and manage uv-managed Python environments as Jupyter kernels.
5
+ Project-URL: Homepage, https://github.com/nOtFoRgEeKs/uv-kernel-manager
6
+ Author: nOtFoRgEeKs
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Environment :: Console
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: jupyter-client>=8.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # uv-kernel-manager
18
+
19
+ `uv-kernel-manager` registers the Python environments created by
20
+ [`uv`](https://docs.astral.sh/uv/) projects as Jupyter kernels. It is designed
21
+ for a single JupyterLab installation shared across many projects, each with
22
+ its own project-local `.venv`.
23
+
24
+ The PyPI distribution is `uv-kernel-manager`; the command is `uv-kernel`; the
25
+ Python package is `uv_kernel`.
26
+
27
+ ## How it works
28
+
29
+ ```
30
+ JupyterLab installation
31
+ │
32
+ ├── Python 3.12 (analytics) ──> analytics/.venv/bin/python
33
+ └── Python 3.13 (forecasting) ─> forecasting/.venv/bin/python
34
+ ```
35
+
36
+ Run `uv-kernel register` from a uv project to create a standard, user-level
37
+ Jupyter kernelspec pointing at that project's interpreter. The command suggests
38
+ a label in the form `Python <major.minor> (<project-name>)`, such as
39
+ `Python 3.13 (forecasting)`, and lets you accept it or provide a label that is
40
+ more meaningful to you.
41
+
42
+ The tool manages kernelspecs only. It does not install, start, configure, or
43
+ upgrade JupyterLab; it also does not create or otherwise manage your uv
44
+ projects or their virtual environments.
45
+
46
+ ## Prerequisites
47
+
48
+ - [uv](https://docs.astral.sh/uv/) on your `PATH`
49
+ - A uv project with a `pyproject.toml` and a synchronized `.venv` (`uv sync`)
50
+ - JupyterLab or another Jupyter frontend installed wherever you normally run it
51
+
52
+ `register` adds `ipykernel` to the current project's development dependencies
53
+ by default. Use `--no-add-ipykernel` only when it is already installed or you
54
+ do not want the command to change project dependencies.
55
+
56
+ ## Installation
57
+
58
+ Install the tool permanently with uv:
59
+
60
+ ```bash
61
+ uv tool install uv-kernel-manager
62
+ ```
63
+
64
+ Then run the `uv-kernel` command from a project directory:
65
+
66
+ ```bash
67
+ cd path/to/forecasting
68
+ uv sync
69
+ uv-kernel register
70
+ jupyter lab
71
+ ```
72
+
73
+ During registration, the tool shows the suggested Jupyter label:
74
+
75
+ ```text
76
+ Register kernel as: Python 3.13 (forecasting)
77
+ Press Enter to continue, or enter a custom name:
78
+ ```
79
+
80
+ Press Enter to use it, or type a custom label. For scripts and other
81
+ non-interactive usage, accept the suggestion with `--yes` or set it explicitly
82
+ with `--name`:
83
+
84
+ ```bash
85
+ uv-kernel register --yes
86
+ uv-kernel register --name "Forecasting — production"
87
+ ```
88
+
89
+ To run without installing it, use `uvx`:
90
+
91
+ ```bash
92
+ uvx uv-kernel-manager register
93
+ uvx uv-kernel-manager status
94
+ ```
95
+
96
+ ## Commands
97
+
98
+ | Command | Purpose |
99
+ | --- | --- |
100
+ | `uv-kernel register` | Prompt for a label and add the current project's `.venv` as a Jupyter kernel. |
101
+ | `uv-kernel register --yes` | Register with the suggested label without prompting. |
102
+ | `uv-kernel register --name "NAME"` | Register with an explicit Jupyter label. |
103
+ | `uv-kernel status` | Show whether the current project is registered. |
104
+ | `uv-kernel refresh` | Recreate the current project's kernel with its current interpreter and settings. |
105
+ | `uv-kernel unregister` | Remove the current project's managed kernel. |
106
+ | `uv-kernel list` | List every kernel managed by this tool. |
107
+ | `uv-kernel clean` | Report managed kernels whose project or interpreter no longer exists. |
108
+ | `uv-kernel clean --yes` | Remove the stale managed kernels reported by `clean`. |
109
+ | `uv-kernel doctor` | Show diagnostic information about uv, the project, and Jupyter kernel locations. |
110
+
111
+ Use `--json` with reporting commands when scripting, for example:
112
+
113
+ ```bash
114
+ uv-kernel --json list
115
+ uv-kernel --json status
116
+ ```
117
+
118
+ For a non-interactive JSON registration, include either `--yes` or `--name`:
119
+
120
+ ```bash
121
+ uv-kernel --json register --yes
122
+ uv-kernel --json register --name "Forecasting — production"
123
+ ```
124
+
125
+ ## Safety and ownership
126
+
127
+ Every kernel created by this tool includes a structured `metadata.uv-kernel`
128
+ ownership record. `unregister`, `refresh`, and `clean` act only on kernels
129
+ with that record; manually installed kernels and kernels managed by other
130
+ tools are left untouched. Kernel directory names also begin with `uv-kernel-`,
131
+ but the metadata marker—not the directory name—is the ownership check.
132
+
133
+ `clean` is intentionally non-destructive unless you explicitly pass `--yes`.
134
+
135
+ ## Build from source
136
+
137
+ To build the package locally, clone the repository and install its development
138
+ dependencies:
139
+
140
+ ```bash
141
+ uv sync --dev
142
+ uv build
143
+ ```
144
+
145
+ The wheel and source distribution are written to `dist/`. To install the local
146
+ checkout as a tool, run:
147
+
148
+ ```bash
149
+ uv tool install .
150
+ ```
151
+
152
+ ## Development
153
+
154
+ Run the test suite and validate built package metadata before contributing:
155
+
156
+ ```bash
157
+ uv run pytest
158
+ uv run twine check dist/*
159
+ ```
160
+
161
+ You can also run the command directly from the checkout:
162
+
163
+ ```bash
164
+ uv run uv-kernel --help
165
+ ```
166
+
167
+ ## Contributing
168
+
169
+ Contributions are welcome. Please open an issue or pull request with a focused
170
+ change, include tests when behavior changes, and run the checks above before
171
+ submitting.
@@ -0,0 +1,8 @@
1
+ uv_kernel/__init__.py,sha256=ZPGIixPEw_hENy_O6J0QtQur9LKh2ciC2R2vCaIea8I,282
2
+ uv_kernel/cli.py,sha256=jAKcDuJlY_ZtZtpECIYgm13W3kRSgWi2EipknVZu4E8,6284
3
+ uv_kernel/core.py,sha256=xNzIrdyVCF8CAc9TZLrLIHGDJuG4sbGo2XWI00QvXw4,6495
4
+ uv_kernel_manager-0.1.1a1.dist-info/METADATA,sha256=LerlZIZ_gp8vyEBZXNk2aqO8tfwXTiSYGe2mNBgkBMY,5380
5
+ uv_kernel_manager-0.1.1a1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ uv_kernel_manager-0.1.1a1.dist-info/entry_points.txt,sha256=JW7hmvs49eP8cG-CMpQpaT5URK6971wZRCr6I0sJP0M,49
7
+ uv_kernel_manager-0.1.1a1.dist-info/licenses/LICENSE,sha256=Gb9LjGPFKEOEdAlCgN0oFgVhKsfo93ASlM8NsftjFG4,1068
8
+ uv_kernel_manager-0.1.1a1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ uv-kernel = uv_kernel.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nOtFoRgEeKs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.