orion-notebook 0.5.0__tar.gz
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.
- orion_notebook-0.5.0/PKG-INFO +78 -0
- orion_notebook-0.5.0/README.md +65 -0
- orion_notebook-0.5.0/orion_agent/__init__.py +3 -0
- orion_notebook-0.5.0/orion_agent/cli.py +394 -0
- orion_notebook-0.5.0/orion_notebook.egg-info/PKG-INFO +78 -0
- orion_notebook-0.5.0/orion_notebook.egg-info/SOURCES.txt +9 -0
- orion_notebook-0.5.0/orion_notebook.egg-info/dependency_links.txt +1 -0
- orion_notebook-0.5.0/orion_notebook.egg-info/entry_points.txt +2 -0
- orion_notebook-0.5.0/orion_notebook.egg-info/top_level.txt +1 -0
- orion_notebook-0.5.0/pyproject.toml +24 -0
- orion_notebook-0.5.0/setup.cfg +4 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orion-notebook
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Local Orion CLI launcher for notebooks and Jupyter
|
|
5
|
+
Author: Nicolas Fonteyne
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# orion-notebook
|
|
15
|
+
|
|
16
|
+
Python launcher for [Orion](https://www.orion-agent.ai). Installs the `orion` command on PyPI.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install orion-notebook
|
|
22
|
+
orion
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What happens on first run
|
|
26
|
+
|
|
27
|
+
The PyPI wheel is intentionally small. When you run `orion` for the first time, the CLI may:
|
|
28
|
+
|
|
29
|
+
1. **Download the Orion app bundle** into `~/.orion/app/<version>` from a GitHub release
|
|
30
|
+
2. **Download portable Node.js 20+** into `~/.orion/runtime/node/<version>` if Node is not installed
|
|
31
|
+
3. **Create an Orion-managed Jupyter venv** under `~/.orion/runtime/venv` if no compatible Jupyter is found
|
|
32
|
+
|
|
33
|
+
Each step prompts for consent unless you pass `--yes`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
orion --yes
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Managed Jupyter is installed only inside Orion's venv, not into your global Python.
|
|
40
|
+
|
|
41
|
+
After the first successful setup, later runs start Jupyter, launch Orion, and open your browser much faster.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Python 3.8+
|
|
46
|
+
- Node.js 20+ (downloaded automatically into `~/.orion/runtime/node` when missing)
|
|
47
|
+
|
|
48
|
+
## Flags
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
orion --yes # auto-approve setup prompts
|
|
52
|
+
orion --no-browser # start services without opening a browser
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Environment variables
|
|
56
|
+
|
|
57
|
+
| Variable | Purpose |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `ORION_HOME_DIR` | Override Orion data root (default: `~/.orion`) |
|
|
60
|
+
| `ORION_APP_BUNDLE_URL` | Override app bundle download URL |
|
|
61
|
+
| `ORION_PORT` | Orion app port (default: `3001`) |
|
|
62
|
+
|
|
63
|
+
Default app bundle URL:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
https://github.com/nicolasakf/Orion-app/releases/download/v<version>/orion-app-<version>.tar.gz
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## npm alternative
|
|
70
|
+
|
|
71
|
+
If you already have Node.js 20+, the npm package is simpler because it ships the app bundle directly:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g @nicolasakf/orion-agent
|
|
75
|
+
orion
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
See the [main README](../README.md) and [Contributing](../CONTRIBUTING.md#publishing-the-cli) for publishing and development details.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# orion-notebook
|
|
2
|
+
|
|
3
|
+
Python launcher for [Orion](https://www.orion-agent.ai). Installs the `orion` command on PyPI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install orion-notebook
|
|
9
|
+
orion
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What happens on first run
|
|
13
|
+
|
|
14
|
+
The PyPI wheel is intentionally small. When you run `orion` for the first time, the CLI may:
|
|
15
|
+
|
|
16
|
+
1. **Download the Orion app bundle** into `~/.orion/app/<version>` from a GitHub release
|
|
17
|
+
2. **Download portable Node.js 20+** into `~/.orion/runtime/node/<version>` if Node is not installed
|
|
18
|
+
3. **Create an Orion-managed Jupyter venv** under `~/.orion/runtime/venv` if no compatible Jupyter is found
|
|
19
|
+
|
|
20
|
+
Each step prompts for consent unless you pass `--yes`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
orion --yes
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Managed Jupyter is installed only inside Orion's venv, not into your global Python.
|
|
27
|
+
|
|
28
|
+
After the first successful setup, later runs start Jupyter, launch Orion, and open your browser much faster.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.8+
|
|
33
|
+
- Node.js 20+ (downloaded automatically into `~/.orion/runtime/node` when missing)
|
|
34
|
+
|
|
35
|
+
## Flags
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
orion --yes # auto-approve setup prompts
|
|
39
|
+
orion --no-browser # start services without opening a browser
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Environment variables
|
|
43
|
+
|
|
44
|
+
| Variable | Purpose |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `ORION_HOME_DIR` | Override Orion data root (default: `~/.orion`) |
|
|
47
|
+
| `ORION_APP_BUNDLE_URL` | Override app bundle download URL |
|
|
48
|
+
| `ORION_PORT` | Orion app port (default: `3001`) |
|
|
49
|
+
|
|
50
|
+
Default app bundle URL:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
https://github.com/nicolasakf/Orion-app/releases/download/v<version>/orion-app-<version>.tar.gz
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## npm alternative
|
|
57
|
+
|
|
58
|
+
If you already have Node.js 20+, the npm package is simpler because it ships the app bundle directly:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install -g @nicolasakf/orion-agent
|
|
62
|
+
orion
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See the [main README](../README.md) and [Contributing](../CONTRIBUTING.md#publishing-the-cli) for publishing and development details.
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Command-line launcher for the PyPI `orion-notebook` package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import secrets
|
|
10
|
+
import shutil
|
|
11
|
+
import socket
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tarfile
|
|
15
|
+
import time
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.request
|
|
18
|
+
import venv
|
|
19
|
+
import webbrowser
|
|
20
|
+
import zipfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
VERSION = "0.5.0"
|
|
25
|
+
NODE_VERSION = "v22.12.0"
|
|
26
|
+
DEFAULT_APP_BUNDLE_URL = (
|
|
27
|
+
f"https://github.com/nicolasakf/Orion-app/releases/download/"
|
|
28
|
+
f"v{VERSION}/orion-app-{VERSION}.tar.gz"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def orion_home() -> Path:
|
|
33
|
+
"""Return Orion's home directory using the `~/.orion` contract."""
|
|
34
|
+
return Path(os.environ.get("ORION_HOME_DIR", Path.home() / ".orion"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def runtime_dir() -> Path:
|
|
38
|
+
"""Return Orion's managed runtime directory."""
|
|
39
|
+
return orion_home() / "runtime"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def app_dir() -> Path:
|
|
43
|
+
"""Return the cached Orion app bundle directory for this version."""
|
|
44
|
+
return orion_home() / "app" / VERSION
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def handoff_path() -> Path:
|
|
48
|
+
"""Return the Jupyter connection handoff path consumed by Orion."""
|
|
49
|
+
return runtime_dir() / "jupyter-connection.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def managed_venv_python() -> Path:
|
|
53
|
+
"""Return the Python executable inside Orion's managed venv."""
|
|
54
|
+
if os.name == "nt":
|
|
55
|
+
return runtime_dir() / "venv" / "Scripts" / "python.exe"
|
|
56
|
+
return runtime_dir() / "venv" / "bin" / "python"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def confirm(message: str, assume_yes: bool) -> bool:
|
|
60
|
+
"""Prompt for setup consent unless `--yes` was provided."""
|
|
61
|
+
if assume_yes:
|
|
62
|
+
return True
|
|
63
|
+
if not sys.stdin.isatty():
|
|
64
|
+
return False
|
|
65
|
+
answer = input(f"{message} [y/N] ").strip().lower()
|
|
66
|
+
return answer in {"y", "yes"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run_checked(command: list[str], cwd: Path | None = None) -> None:
|
|
70
|
+
"""Run a subprocess command and raise when it fails."""
|
|
71
|
+
subprocess.run(command, cwd=cwd, check=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_node_version(output: str) -> tuple[int, int, int] | None:
|
|
75
|
+
"""Parse a Node.js `vX.Y.Z` version string."""
|
|
76
|
+
text = output.strip().lstrip("v")
|
|
77
|
+
parts = text.split(".")
|
|
78
|
+
if len(parts) < 3:
|
|
79
|
+
return None
|
|
80
|
+
try:
|
|
81
|
+
return int(parts[0]), int(parts[1]), int(parts[2])
|
|
82
|
+
except ValueError:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def system_node() -> str | None:
|
|
87
|
+
"""Return a usable system Node executable, or None if missing/too old."""
|
|
88
|
+
node = shutil.which("node")
|
|
89
|
+
if not node:
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
[node, "--version"],
|
|
94
|
+
check=True,
|
|
95
|
+
text=True,
|
|
96
|
+
capture_output=True,
|
|
97
|
+
)
|
|
98
|
+
except (OSError, subprocess.CalledProcessError):
|
|
99
|
+
return None
|
|
100
|
+
version = parse_node_version(result.stdout)
|
|
101
|
+
if version and version[0] >= 20:
|
|
102
|
+
return node
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def node_platform_slug() -> tuple[str, str]:
|
|
107
|
+
"""Return the Node distribution platform slug and archive extension."""
|
|
108
|
+
machine = platform.machine().lower()
|
|
109
|
+
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64"
|
|
110
|
+
if sys.platform == "win32":
|
|
111
|
+
return f"win-{arch}", "zip"
|
|
112
|
+
if sys.platform == "darwin":
|
|
113
|
+
return f"darwin-{arch}", "tar.gz"
|
|
114
|
+
return f"linux-{arch}", "tar.gz"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def download_file(url: str, destination: Path) -> None:
|
|
118
|
+
"""Download a URL to a destination path."""
|
|
119
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
with urllib.request.urlopen(url) as response, destination.open("wb") as handle:
|
|
121
|
+
shutil.copyfileobj(response, handle)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def ensure_node(assume_yes: bool) -> str:
|
|
125
|
+
"""Return a Node 20+ executable, downloading portable Node when needed."""
|
|
126
|
+
existing = system_node()
|
|
127
|
+
if existing:
|
|
128
|
+
return existing
|
|
129
|
+
|
|
130
|
+
if not confirm(
|
|
131
|
+
"Orion needs Node.js 20+ to run the local app. Download portable Node into ~/.orion/runtime?",
|
|
132
|
+
assume_yes,
|
|
133
|
+
):
|
|
134
|
+
raise SystemExit("Setup declined. Install Node.js 20+ or rerun `orion --yes`.")
|
|
135
|
+
|
|
136
|
+
slug, ext = node_platform_slug()
|
|
137
|
+
node_root = runtime_dir() / "node" / NODE_VERSION
|
|
138
|
+
extracted = node_root / f"node-{NODE_VERSION}-{slug}"
|
|
139
|
+
node_path = extracted / ("node.exe" if sys.platform == "win32" else "bin/node")
|
|
140
|
+
if node_path.exists():
|
|
141
|
+
return str(node_path)
|
|
142
|
+
|
|
143
|
+
archive = node_root / f"node-{NODE_VERSION}-{slug}.{ext}"
|
|
144
|
+
url = f"https://nodejs.org/dist/{NODE_VERSION}/node-{NODE_VERSION}-{slug}.{ext}"
|
|
145
|
+
print(f"Downloading Node.js from {url}")
|
|
146
|
+
download_file(url, archive)
|
|
147
|
+
if ext == "zip":
|
|
148
|
+
with zipfile.ZipFile(archive) as zf:
|
|
149
|
+
zf.extractall(node_root)
|
|
150
|
+
else:
|
|
151
|
+
with tarfile.open(archive) as tf:
|
|
152
|
+
tf.extractall(node_root)
|
|
153
|
+
return str(node_path)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ensure_app_bundle(assume_yes: bool) -> Path:
|
|
157
|
+
"""Return the Orion app bundle directory, downloading it when absent."""
|
|
158
|
+
directory = app_dir()
|
|
159
|
+
if (directory / "server.js").exists():
|
|
160
|
+
return directory
|
|
161
|
+
|
|
162
|
+
url = os.environ.get("ORION_APP_BUNDLE_URL", DEFAULT_APP_BUNDLE_URL)
|
|
163
|
+
if not confirm(
|
|
164
|
+
f"Download the Orion app bundle into {directory}?",
|
|
165
|
+
assume_yes,
|
|
166
|
+
):
|
|
167
|
+
raise SystemExit("Setup declined. Set ORION_APP_BUNDLE_URL or install through npm.")
|
|
168
|
+
|
|
169
|
+
archive = runtime_dir() / "downloads" / f"orion-app-{VERSION}.tar.gz"
|
|
170
|
+
print(f"Downloading Orion app bundle from {url}")
|
|
171
|
+
download_file(url, archive)
|
|
172
|
+
directory.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
if directory.exists():
|
|
174
|
+
shutil.rmtree(directory)
|
|
175
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
with tarfile.open(archive) as tf:
|
|
177
|
+
tf.extractall(directory)
|
|
178
|
+
if not (directory / "server.js").exists():
|
|
179
|
+
children = [child for child in directory.iterdir()]
|
|
180
|
+
if len(children) == 1 and (children[0] / "server.js").exists():
|
|
181
|
+
extracted_root = children[0]
|
|
182
|
+
for child in extracted_root.iterdir():
|
|
183
|
+
shutil.move(str(child), directory)
|
|
184
|
+
extracted_root.rmdir()
|
|
185
|
+
if not (directory / "server.js").exists():
|
|
186
|
+
raise SystemExit("Downloaded Orion app bundle did not contain server.js.")
|
|
187
|
+
return directory
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def has_jupyter(python: str) -> bool:
|
|
191
|
+
"""Return whether a Python executable can import Jupyter Server."""
|
|
192
|
+
try:
|
|
193
|
+
subprocess.run(
|
|
194
|
+
[python, "-c", "import jupyter_server"],
|
|
195
|
+
check=True,
|
|
196
|
+
stdout=subprocess.DEVNULL,
|
|
197
|
+
stderr=subprocess.DEVNULL,
|
|
198
|
+
)
|
|
199
|
+
return True
|
|
200
|
+
except (OSError, subprocess.CalledProcessError):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def install_managed_jupyter(assume_yes: bool) -> str:
|
|
205
|
+
"""Create/update Orion's managed venv and install Jupyter packages there."""
|
|
206
|
+
py = managed_venv_python()
|
|
207
|
+
if not py.exists():
|
|
208
|
+
if not confirm(
|
|
209
|
+
"Orion needs a local Jupyter runtime. Create it under ~/.orion/runtime?",
|
|
210
|
+
assume_yes,
|
|
211
|
+
):
|
|
212
|
+
raise SystemExit("Setup declined. Install Jupyter or rerun `orion --yes`.")
|
|
213
|
+
runtime_dir().mkdir(parents=True, exist_ok=True)
|
|
214
|
+
venv.EnvBuilder(with_pip=True).create(runtime_dir() / "venv")
|
|
215
|
+
|
|
216
|
+
packages = [
|
|
217
|
+
"jupyter_server>=1.24,<3",
|
|
218
|
+
"jupyter_server_terminals>=0.4,<1",
|
|
219
|
+
"ipykernel>=6,<7",
|
|
220
|
+
]
|
|
221
|
+
run_checked([str(py), "-m", "pip", "install", "--upgrade", "pip", *packages])
|
|
222
|
+
return str(py)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def free_port() -> int:
|
|
226
|
+
"""Return a free port bound to 127.0.0.1."""
|
|
227
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
228
|
+
sock.bind(("127.0.0.1", 0))
|
|
229
|
+
return int(sock.getsockname()[1])
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def jupyter_json(base_url: str, endpoint: str, token: str) -> Any:
|
|
233
|
+
"""Fetch JSON from a Jupyter API endpoint with token auth."""
|
|
234
|
+
url = f"{base_url.rstrip('/')}/{endpoint}?token={token}"
|
|
235
|
+
request = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
|
|
236
|
+
with urllib.request.urlopen(request, timeout=5) as response:
|
|
237
|
+
return json.loads(response.read().decode("utf-8"))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def start_jupyter(
|
|
241
|
+
python: str,
|
|
242
|
+
cwd: Path | None = None,
|
|
243
|
+
) -> tuple[subprocess.Popen[bytes], str, str, dict[str, bool], str]:
|
|
244
|
+
"""Start Jupyter Server and return process, URL, token, capabilities, version."""
|
|
245
|
+
port = free_port()
|
|
246
|
+
token = secrets.token_hex(24)
|
|
247
|
+
base_url = f"http://127.0.0.1:{port}/"
|
|
248
|
+
proc = subprocess.Popen(
|
|
249
|
+
[
|
|
250
|
+
python,
|
|
251
|
+
"-m",
|
|
252
|
+
"jupyter_server",
|
|
253
|
+
"--no-browser",
|
|
254
|
+
"--ip=127.0.0.1",
|
|
255
|
+
f"--port={port}",
|
|
256
|
+
f"--ServerApp.token={token}",
|
|
257
|
+
"--ServerApp.allow_origin=*",
|
|
258
|
+
"--ServerApp.disable_check_xsrf=True",
|
|
259
|
+
],
|
|
260
|
+
cwd=cwd or Path.home(),
|
|
261
|
+
stdout=subprocess.PIPE,
|
|
262
|
+
stderr=subprocess.PIPE,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
deadline = time.time() + 90
|
|
266
|
+
while time.time() < deadline:
|
|
267
|
+
if proc.poll() not in (None, 0):
|
|
268
|
+
raise SystemExit("Jupyter exited before it became ready.")
|
|
269
|
+
try:
|
|
270
|
+
api = jupyter_json(base_url, "api", token)
|
|
271
|
+
break
|
|
272
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
|
|
273
|
+
time.sleep(0.3)
|
|
274
|
+
else:
|
|
275
|
+
proc.terminate()
|
|
276
|
+
raise SystemExit("Jupyter did not become ready before the timeout.")
|
|
277
|
+
|
|
278
|
+
capabilities = {
|
|
279
|
+
"kernelspecs": isinstance(jupyter_json(base_url, "api/kernelspecs", token).get("kernelspecs"), dict),
|
|
280
|
+
"sessions": isinstance(jupyter_json(base_url, "api/sessions", token), list),
|
|
281
|
+
"kernels": isinstance(jupyter_json(base_url, "api/kernels", token), list),
|
|
282
|
+
"contents": isinstance(jupyter_json(base_url, "api/contents", token), dict),
|
|
283
|
+
"terminals": isinstance(jupyter_json(base_url, "api/terminals", token), list),
|
|
284
|
+
"sysInfo": False,
|
|
285
|
+
}
|
|
286
|
+
try:
|
|
287
|
+
jupyter_json(base_url, "api/sys_info", token)
|
|
288
|
+
capabilities["sysInfo"] = True
|
|
289
|
+
except Exception:
|
|
290
|
+
capabilities["sysInfo"] = False
|
|
291
|
+
|
|
292
|
+
missing = [name for name in ("kernelspecs", "sessions", "kernels", "contents", "terminals") if not capabilities[name]]
|
|
293
|
+
if missing:
|
|
294
|
+
proc.terminate()
|
|
295
|
+
raise SystemExit(f"Jupyter is missing required APIs: {', '.join(missing)}")
|
|
296
|
+
version = str(api.get("version") or api.get("server_version") or api.get("jupyter_server_version") or "unknown")
|
|
297
|
+
return proc, base_url, token, capabilities, version
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def write_handoff(
|
|
301
|
+
base_url: str,
|
|
302
|
+
token: str,
|
|
303
|
+
python: str,
|
|
304
|
+
capabilities: dict[str, bool],
|
|
305
|
+
version: str,
|
|
306
|
+
source: str,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Write the Jupyter connection handoff consumed by the Orion app."""
|
|
309
|
+
handoff_path().parent.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
handoff_path().write_text(
|
|
311
|
+
json.dumps(
|
|
312
|
+
{
|
|
313
|
+
"baseUrl": base_url,
|
|
314
|
+
"token": token,
|
|
315
|
+
"source": source,
|
|
316
|
+
"pythonPath": python,
|
|
317
|
+
"jupyterVersion": version,
|
|
318
|
+
"capabilities": capabilities,
|
|
319
|
+
"createdAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
320
|
+
},
|
|
321
|
+
indent=2,
|
|
322
|
+
)
|
|
323
|
+
+ "\n",
|
|
324
|
+
encoding="utf-8",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def start_orion_app(node: str, app: Path) -> tuple[subprocess.Popen[bytes], str]:
|
|
329
|
+
"""Start the local Orion Next server."""
|
|
330
|
+
port = int(os.environ.get("ORION_PORT", "3001"))
|
|
331
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
332
|
+
if sock.connect_ex(("127.0.0.1", port)) == 0:
|
|
333
|
+
port = free_port()
|
|
334
|
+
|
|
335
|
+
env = {
|
|
336
|
+
**os.environ,
|
|
337
|
+
"HOSTNAME": "127.0.0.1",
|
|
338
|
+
"NODE_ENV": "production",
|
|
339
|
+
"PORT": str(port),
|
|
340
|
+
}
|
|
341
|
+
proc = subprocess.Popen([node, str(app / "server.js")], cwd=app, env=env)
|
|
342
|
+
return proc, f"http://127.0.0.1:{port}"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def main() -> None:
|
|
346
|
+
"""Run the PyPI Orion CLI entrypoint."""
|
|
347
|
+
parser = argparse.ArgumentParser(description="Start Orion locally.")
|
|
348
|
+
parser.add_argument("-y", "--yes", action="store_true", help="Approve setup prompts.")
|
|
349
|
+
parser.add_argument("--no-browser", action="store_true", help="Do not open a browser.")
|
|
350
|
+
parser.add_argument(
|
|
351
|
+
"--here",
|
|
352
|
+
action="store_true",
|
|
353
|
+
help="Start Jupyter from the current directory instead of ~.",
|
|
354
|
+
)
|
|
355
|
+
args = parser.parse_args()
|
|
356
|
+
|
|
357
|
+
jupyter_root = Path.cwd() if args.here else Path.home()
|
|
358
|
+
|
|
359
|
+
app = ensure_app_bundle(args.yes)
|
|
360
|
+
node = ensure_node(args.yes)
|
|
361
|
+
uses_existing_jupyter = has_jupyter(sys.executable)
|
|
362
|
+
python = sys.executable if uses_existing_jupyter else install_managed_jupyter(args.yes)
|
|
363
|
+
try:
|
|
364
|
+
jupyter_proc, base_url, token, capabilities, version = start_jupyter(python, jupyter_root)
|
|
365
|
+
except SystemExit:
|
|
366
|
+
if not uses_existing_jupyter:
|
|
367
|
+
raise
|
|
368
|
+
print("Existing Jupyter is not compatible with Orion. Falling back to an Orion-managed runtime.")
|
|
369
|
+
python = install_managed_jupyter(args.yes)
|
|
370
|
+
uses_existing_jupyter = False
|
|
371
|
+
jupyter_proc, base_url, token, capabilities, version = start_jupyter(python, jupyter_root)
|
|
372
|
+
write_handoff(
|
|
373
|
+
base_url,
|
|
374
|
+
token,
|
|
375
|
+
python,
|
|
376
|
+
capabilities,
|
|
377
|
+
version,
|
|
378
|
+
"existing" if uses_existing_jupyter else "managed",
|
|
379
|
+
)
|
|
380
|
+
app_proc, url = start_orion_app(node, app)
|
|
381
|
+
|
|
382
|
+
print(f"Orion is running at {url}")
|
|
383
|
+
print(f"Jupyter is running at {base_url} (root: {jupyter_root})")
|
|
384
|
+
if not args.no_browser:
|
|
385
|
+
webbrowser.open(url)
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
app_proc.wait()
|
|
389
|
+
finally:
|
|
390
|
+
jupyter_proc.terminate()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
if __name__ == "__main__":
|
|
394
|
+
main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orion-notebook
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Local Orion CLI launcher for notebooks and Jupyter
|
|
5
|
+
Author: Nicolas Fonteyne
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# orion-notebook
|
|
15
|
+
|
|
16
|
+
Python launcher for [Orion](https://www.orion-agent.ai). Installs the `orion` command on PyPI.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install orion-notebook
|
|
22
|
+
orion
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What happens on first run
|
|
26
|
+
|
|
27
|
+
The PyPI wheel is intentionally small. When you run `orion` for the first time, the CLI may:
|
|
28
|
+
|
|
29
|
+
1. **Download the Orion app bundle** into `~/.orion/app/<version>` from a GitHub release
|
|
30
|
+
2. **Download portable Node.js 20+** into `~/.orion/runtime/node/<version>` if Node is not installed
|
|
31
|
+
3. **Create an Orion-managed Jupyter venv** under `~/.orion/runtime/venv` if no compatible Jupyter is found
|
|
32
|
+
|
|
33
|
+
Each step prompts for consent unless you pass `--yes`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
orion --yes
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Managed Jupyter is installed only inside Orion's venv, not into your global Python.
|
|
40
|
+
|
|
41
|
+
After the first successful setup, later runs start Jupyter, launch Orion, and open your browser much faster.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Python 3.8+
|
|
46
|
+
- Node.js 20+ (downloaded automatically into `~/.orion/runtime/node` when missing)
|
|
47
|
+
|
|
48
|
+
## Flags
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
orion --yes # auto-approve setup prompts
|
|
52
|
+
orion --no-browser # start services without opening a browser
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Environment variables
|
|
56
|
+
|
|
57
|
+
| Variable | Purpose |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `ORION_HOME_DIR` | Override Orion data root (default: `~/.orion`) |
|
|
60
|
+
| `ORION_APP_BUNDLE_URL` | Override app bundle download URL |
|
|
61
|
+
| `ORION_PORT` | Orion app port (default: `3001`) |
|
|
62
|
+
|
|
63
|
+
Default app bundle URL:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
https://github.com/nicolasakf/Orion-app/releases/download/v<version>/orion-app-<version>.tar.gz
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## npm alternative
|
|
70
|
+
|
|
71
|
+
If you already have Node.js 20+, the npm package is simpler because it ships the app bundle directly:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g @nicolasakf/orion-agent
|
|
75
|
+
orion
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
See the [main README](../README.md) and [Contributing](../CONTRIBUTING.md#publishing-the-cli) for publishing and development details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
orion_agent/__init__.py
|
|
4
|
+
orion_agent/cli.py
|
|
5
|
+
orion_notebook.egg-info/PKG-INFO
|
|
6
|
+
orion_notebook.egg-info/SOURCES.txt
|
|
7
|
+
orion_notebook.egg-info/dependency_links.txt
|
|
8
|
+
orion_notebook.egg-info/entry_points.txt
|
|
9
|
+
orion_notebook.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
orion_agent
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orion-notebook"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Local Orion CLI launcher for notebooks and Jupyter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Nicolas Fonteyne" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
orion = "orion_agent.cli:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["orion_agent"]
|