kedi-notebook 0.1.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.
Files changed (47) hide show
  1. kedi_notebook-0.1.0/.gitignore +7 -0
  2. kedi_notebook-0.1.0/LICENSE +21 -0
  3. kedi_notebook-0.1.0/PKG-INFO +169 -0
  4. kedi_notebook-0.1.0/README.md +153 -0
  5. kedi_notebook-0.1.0/pyproject.toml +63 -0
  6. kedi_notebook-0.1.0/src/kedi_notebook/__init__.py +15 -0
  7. kedi_notebook-0.1.0/src/kedi_notebook/__main__.py +4 -0
  8. kedi_notebook-0.1.0/src/kedi_notebook/bridge.py +171 -0
  9. kedi_notebook-0.1.0/src/kedi_notebook/execution.py +91 -0
  10. kedi_notebook-0.1.0/src/kedi_notebook/host_environment.py +233 -0
  11. kedi_notebook-0.1.0/src/kedi_notebook/model_dependencies.py +95 -0
  12. kedi_notebook-0.1.0/src/kedi_notebook/pyright.py +605 -0
  13. kedi_notebook-0.1.0/src/kedi_notebook/runtime.py +928 -0
  14. kedi_notebook-0.1.0/src/kedi_notebook/sandbox_worker.py +339 -0
  15. kedi_notebook-0.1.0/src/kedi_notebook/secrets.py +158 -0
  16. kedi_notebook-0.1.0/src/kedi_notebook/server.py +865 -0
  17. kedi_notebook-0.1.0/src/kedi_notebook/static/grammars/kedi/highlights.scm +275 -0
  18. kedi_notebook-0.1.0/src/kedi_notebook/static/grammars/kedi/injections.scm +23 -0
  19. kedi_notebook-0.1.0/src/kedi_notebook/static/grammars/kedi/tree-sitter-kedi.wasm +0 -0
  20. kedi_notebook-0.1.0/src/kedi_notebook/static/grammars/python/highlights.scm +137 -0
  21. kedi_notebook-0.1.0/src/kedi_notebook/static/grammars/python/tree-sitter-python.wasm +0 -0
  22. kedi_notebook-0.1.0/src/kedi_notebook/static/kedi-editor.js +695 -0
  23. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/assets/LICENSE +93 -0
  24. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/assets/geist-latin-wght-normal.woff2 +0 -0
  25. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/assets/geist-mono-latin-wght-normal.woff2 +0 -0
  26. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/assets/kedi-logo-white.svg +9 -0
  27. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/index.html +196 -0
  28. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/notebook.css +1126 -0
  29. kedi_notebook-0.1.0/src/kedi_notebook/static/notebook/notebook.js +2007 -0
  30. kedi_notebook-0.1.0/src/kedi_notebook/static/pyodide-runtime.js +257 -0
  31. kedi_notebook-0.1.0/src/kedi_notebook/static/pyodide-worker.js +580 -0
  32. kedi_notebook-0.1.0/src/kedi_notebook/static/tree-sitter-highlighter.js +461 -0
  33. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/lucide.min.js +12 -0
  34. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/LICENSE +373 -0
  35. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/annotated_types-0.7.0-py3-none-any.whl +0 -0
  36. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/micropip-0.11.1-py3-none-any.whl +0 -0
  37. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/pydantic-2.12.5-py3-none-any.whl +0 -0
  38. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/pydantic_core-2.41.5-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
  39. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/pyodide-lock.json +1 -0
  40. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/pyodide.asm.mjs +2 -0
  41. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/pyodide.asm.wasm +0 -0
  42. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/pyodide.mjs +4 -0
  43. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/python_stdlib.zip +0 -0
  44. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/typing_extensions-4.15.0-py3-none-any.whl +0 -0
  45. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/pyodide/typing_inspection-0.4.2-py3-none-any.whl +0 -0
  46. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/web-tree-sitter.js +4007 -0
  47. kedi_notebook-0.1.0/src/kedi_notebook/static/vendor/web-tree-sitter.wasm +0 -0
@@ -0,0 +1,7 @@
1
+ .coverage
2
+ .DS_Store
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ __pycache__/
7
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kedi Programming Language
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.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.5
2
+ Name: kedi-notebook
3
+ Version: 0.1.0
4
+ Summary: Local interactive notebook for the Kedi Programming Language
5
+ Project-URL: Source, https://github.com/kedi-lang/notebook
6
+ Project-URL: Tracker, https://github.com/kedi-lang/notebook/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: basedpyright<1.41,>=1.40.2
11
+ Requires-Dist: fastapi<0.163,>=0.162.0
12
+ Requires-Dist: kedi<0.2,>=0.1.2
13
+ Requires-Dist: python-dotenv<2,>=1.2.4
14
+ Requires-Dist: uvicorn<0.55,>=0.54
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Kedi Notebook
18
+
19
+ Kedi Notebook is the local, incremental notebook interface for the Kedi
20
+ Programming Language. It provides editable Kedi, terminal, and Markdown cells,
21
+ with either browser-owned Pyodide or a selected host Python interpreter.
22
+
23
+ ![Kedi Notebook running incremental Kedi cells](.github/assets/kedi-notebook.png)
24
+
25
+ ## Install and Run
26
+
27
+ The notebook is currently distributed from a Kedi source checkout. Install its
28
+ local dependencies and start it in one command from the Kedi repository root:
29
+
30
+ ```bash
31
+ uv run --extra notebook kedi notebook
32
+ ```
33
+
34
+ This uses the checked-out `notebook` submodule and Kedi source rather than a
35
+ published package. For notebook development, run from the Kedi repository root
36
+ with the notebook extra and development dependencies installed:
37
+
38
+ ```bash
39
+ .venv/bin/python -m pytest notebook/tests
40
+ ```
41
+
42
+ After syncing the extra, both entry points are equivalent:
43
+
44
+ ```bash
45
+ kedi notebook
46
+ kedi-notebook
47
+ ```
48
+
49
+ The default address is <http://127.0.0.1:8788/notebook/>. Add one or more host
50
+ Python installations explicitly when needed:
51
+
52
+ ```bash
53
+ kedi-notebook --python /opt/homebrew/bin/python3.11
54
+ kedi notebook --python ~/.pyenv/versions/3.12.4/bin/python --port 8899
55
+ ```
56
+
57
+ Use `--host`, `--port`, `--cwd`, and `--no-open` to control local serving.
58
+ Loopback serving requires no credentials. A non-loopback `--host` is rejected
59
+ unless `--token` or `KEDI_NOTEBOOK_TOKEN` is set; the browser opened by the CLI
60
+ receives that token without leaving it in the visible URL.
61
+
62
+ At startup, the notebook server loads `.env` from `--cwd` without overriding
63
+ variables already present in its process. Set `KEDI_ADAPTER_MODEL` there or use
64
+ `> model:` in a Kedi cell before invoking a template. Calling `load_dotenv()`
65
+ inside a host Python cell affects only that cell worker, not the server process
66
+ that owns adapters; browser Python cannot read the host project's `.env`.
67
+
68
+ When a native Pydantic AI model requires a missing provider SDK, the notebook
69
+ installs only the provider group named by Pydantic AI, immediately before model
70
+ construction. OpenRouter, for example, requests the `openai` group. Unused
71
+ provider SDKs are not installed. Installation targets the notebook server's
72
+ Python environment, keeps its existing package versions, and requires package
73
+ index access on first use. It does not replay earlier statements in the cell.
74
+ Provider credentials are still required. A dependency conflict or failed
75
+ installation is shown as a cell error; specialized adapters retain their own
76
+ installation requirements.
77
+
78
+ The **Secret Manager** action in the top bar stores environment values outside
79
+ the notebook at `~/.kedi/notebook/secrets.json`. Stored values take precedence
80
+ over the server process and project `.env`, and the browser can list only their
81
+ names, never their values. Add a single value or import an arbitrary `.env`
82
+ path; relative paths resolve from `--cwd`. The store is atomically written with
83
+ user-only file permissions and is never included in browser storage. Saving,
84
+ importing, or deleting a value closes active sessions so the next cell starts a
85
+ runtime with the new environment.
86
+
87
+ ## Runtime Model
88
+
89
+ Browser mode starts one persistent Pyodide worker as the page loads. Host mode
90
+ creates and reuses a project-specific virtual environment from the selected
91
+ Python executable, installs Kedi into it, and starts one persistent worker from
92
+ that environment. Kedi cells
93
+ execute incrementally against that session; completed cells remain editable
94
+ and rerunnable, and output is displayed under its source.
95
+ The bundled `filesystem` module works in both modes: browser mode accesses the
96
+ worker's temporary virtual filesystem, while host mode accesses the notebook
97
+ working directory on disk.
98
+
99
+ Every visible cell keeps its editor when focus moves elsewhere; inactive Kedi
100
+ cells retain syntax highlighting and can be edited or run directly. The cell
101
+ header can convert an individual cell between Kedi, Markdown, and Terminal.
102
+ Cells never collapse implicitly. Use the eye control to hide a cell explicitly;
103
+ the compact row remains available to show it again, and hidden state is saved
104
+ in `.kedinb` documents and local drafts.
105
+
106
+ Managed environments live under `~/.kedi/notebook/venvs` by default and have a
107
+ stable `kedi-notebook-py...` name derived from the base interpreter and working
108
+ directory. Set `KEDI_NOTEBOOK_ENV_HOME` to choose another root. The package
109
+ action beside a host runtime lists installed packages and installs newline-
110
+ separated requirement strings into the managed environment with streamed pip
111
+ output. The environment and installed packages persist across notebook server
112
+ runs while the directory remains available.
113
+
114
+ Cells beginning with `!` are terminal cells. Host commands run in the notebook
115
+ working directory, with `!python` and `!pip` bound to the managed environment.
116
+ Browser mode supports `!pip install`, `!uv add`, `!pip list`, `!echo`, and
117
+ `!pwd`. Terminal output streams into the active cell while it runs.
118
+
119
+ Use the square interrupt action while a cell is running. Interrupting replaces
120
+ the active Python worker, so the source remains rerunnable but live runtime
121
+ state is deliberately discarded. It also cancels that session's in-flight
122
+ Pydantic AI or LangChain model and async tool work, without cancelling another
123
+ session. Cancellation does not roll back completed effects or forcibly stop
124
+ arbitrary synchronous tool code running in a server thread.
125
+
126
+ Embedded-Python bridge operations and terminal execution have 120-second
127
+ timeouts; this is not a universal deadline for a complete model-backed cell.
128
+ Package installation has a 10-minute limit. Idle sessions expire after 30 minutes.
129
+
130
+ Notebook execution is non-transactional. Rerunning a cell is a new execution
131
+ attempt against current state; it does not roll back previous side effects.
132
+ Displayed cell numbers follow notebook order and remain unchanged across
133
+ reruns. Adding, moving, or deleting cells recomputes affected positions.
134
+
135
+ Notebook files are validated before they replace the current document. The UI
136
+ warns before discarding unsaved changes and limits files to 5 MB, notebooks to
137
+ 1,000 cells, individual cell source to 1 MB, and retained inline output to
138
+ 200,000 characters. Output beyond that boundary is marked as truncated.
139
+
140
+ Downloading asks which form to save. **Just notebook** writes cell sources,
141
+ types, visibility, and layout without outputs or environment values. **Save
142
+ progress** also writes retained cell outputs/results and the strict,
143
+ pickle-free `InteractiveSession` snapshot containing the current KediEnv. It
144
+ never includes Secret Manager or process environment values. Opening a progress
145
+ file restores the Kedi session on the next execution. If any live value cannot
146
+ be represented without changing its semantics, progress download is rejected
147
+ instead of producing a partial snapshot.
148
+
149
+ ## Editing
150
+
151
+ Kedi cells provide Tree-sitter highlighting, Kedi and embedded-Python
152
+ completion, hover, references, rename, live diagnostics, signature help,
153
+ definition navigation, and runtime error markers. Rename is limited to the
154
+ active cell when its LSP context includes earlier executed cells, avoiding a
155
+ silent partial edit. Monaco falls back to a plain multiline editor if its external assets
156
+ cannot be loaded. Markdown cells safely render headings, paragraphs, lists,
157
+ fenced code, blockquotes, emphasis, links, and separators; raw HTML is not
158
+ executed.
159
+
160
+ Use `Shift+Enter` to run the active cell, `Ctrl/Cmd+S` to download the notebook,
161
+ `Ctrl/Cmd+Enter` to insert after the active cell, `Ctrl/Cmd+Shift+Enter` to
162
+ append, `Ctrl/Cmd+Backspace` to delete, and `Ctrl/Cmd+ArrowUp/ArrowDown` to
163
+ move a cell. Toolbar actions expose the same operations.
164
+
165
+ Monaco is currently pinned on jsDelivr. Pyodide, its standard library, Micropip,
166
+ and Pydantic are vendored in the notebook wheel, so browser Python can start
167
+ without downloading its core runtime from a CDN. Packages installed later with
168
+ Micropip may still need network access. Host execution remains usable through
169
+ the fallback editor if Monaco is unavailable.
@@ -0,0 +1,153 @@
1
+ # Kedi Notebook
2
+
3
+ Kedi Notebook is the local, incremental notebook interface for the Kedi
4
+ Programming Language. It provides editable Kedi, terminal, and Markdown cells,
5
+ with either browser-owned Pyodide or a selected host Python interpreter.
6
+
7
+ ![Kedi Notebook running incremental Kedi cells](.github/assets/kedi-notebook.png)
8
+
9
+ ## Install and Run
10
+
11
+ The notebook is currently distributed from a Kedi source checkout. Install its
12
+ local dependencies and start it in one command from the Kedi repository root:
13
+
14
+ ```bash
15
+ uv run --extra notebook kedi notebook
16
+ ```
17
+
18
+ This uses the checked-out `notebook` submodule and Kedi source rather than a
19
+ published package. For notebook development, run from the Kedi repository root
20
+ with the notebook extra and development dependencies installed:
21
+
22
+ ```bash
23
+ .venv/bin/python -m pytest notebook/tests
24
+ ```
25
+
26
+ After syncing the extra, both entry points are equivalent:
27
+
28
+ ```bash
29
+ kedi notebook
30
+ kedi-notebook
31
+ ```
32
+
33
+ The default address is <http://127.0.0.1:8788/notebook/>. Add one or more host
34
+ Python installations explicitly when needed:
35
+
36
+ ```bash
37
+ kedi-notebook --python /opt/homebrew/bin/python3.11
38
+ kedi notebook --python ~/.pyenv/versions/3.12.4/bin/python --port 8899
39
+ ```
40
+
41
+ Use `--host`, `--port`, `--cwd`, and `--no-open` to control local serving.
42
+ Loopback serving requires no credentials. A non-loopback `--host` is rejected
43
+ unless `--token` or `KEDI_NOTEBOOK_TOKEN` is set; the browser opened by the CLI
44
+ receives that token without leaving it in the visible URL.
45
+
46
+ At startup, the notebook server loads `.env` from `--cwd` without overriding
47
+ variables already present in its process. Set `KEDI_ADAPTER_MODEL` there or use
48
+ `> model:` in a Kedi cell before invoking a template. Calling `load_dotenv()`
49
+ inside a host Python cell affects only that cell worker, not the server process
50
+ that owns adapters; browser Python cannot read the host project's `.env`.
51
+
52
+ When a native Pydantic AI model requires a missing provider SDK, the notebook
53
+ installs only the provider group named by Pydantic AI, immediately before model
54
+ construction. OpenRouter, for example, requests the `openai` group. Unused
55
+ provider SDKs are not installed. Installation targets the notebook server's
56
+ Python environment, keeps its existing package versions, and requires package
57
+ index access on first use. It does not replay earlier statements in the cell.
58
+ Provider credentials are still required. A dependency conflict or failed
59
+ installation is shown as a cell error; specialized adapters retain their own
60
+ installation requirements.
61
+
62
+ The **Secret Manager** action in the top bar stores environment values outside
63
+ the notebook at `~/.kedi/notebook/secrets.json`. Stored values take precedence
64
+ over the server process and project `.env`, and the browser can list only their
65
+ names, never their values. Add a single value or import an arbitrary `.env`
66
+ path; relative paths resolve from `--cwd`. The store is atomically written with
67
+ user-only file permissions and is never included in browser storage. Saving,
68
+ importing, or deleting a value closes active sessions so the next cell starts a
69
+ runtime with the new environment.
70
+
71
+ ## Runtime Model
72
+
73
+ Browser mode starts one persistent Pyodide worker as the page loads. Host mode
74
+ creates and reuses a project-specific virtual environment from the selected
75
+ Python executable, installs Kedi into it, and starts one persistent worker from
76
+ that environment. Kedi cells
77
+ execute incrementally against that session; completed cells remain editable
78
+ and rerunnable, and output is displayed under its source.
79
+ The bundled `filesystem` module works in both modes: browser mode accesses the
80
+ worker's temporary virtual filesystem, while host mode accesses the notebook
81
+ working directory on disk.
82
+
83
+ Every visible cell keeps its editor when focus moves elsewhere; inactive Kedi
84
+ cells retain syntax highlighting and can be edited or run directly. The cell
85
+ header can convert an individual cell between Kedi, Markdown, and Terminal.
86
+ Cells never collapse implicitly. Use the eye control to hide a cell explicitly;
87
+ the compact row remains available to show it again, and hidden state is saved
88
+ in `.kedinb` documents and local drafts.
89
+
90
+ Managed environments live under `~/.kedi/notebook/venvs` by default and have a
91
+ stable `kedi-notebook-py...` name derived from the base interpreter and working
92
+ directory. Set `KEDI_NOTEBOOK_ENV_HOME` to choose another root. The package
93
+ action beside a host runtime lists installed packages and installs newline-
94
+ separated requirement strings into the managed environment with streamed pip
95
+ output. The environment and installed packages persist across notebook server
96
+ runs while the directory remains available.
97
+
98
+ Cells beginning with `!` are terminal cells. Host commands run in the notebook
99
+ working directory, with `!python` and `!pip` bound to the managed environment.
100
+ Browser mode supports `!pip install`, `!uv add`, `!pip list`, `!echo`, and
101
+ `!pwd`. Terminal output streams into the active cell while it runs.
102
+
103
+ Use the square interrupt action while a cell is running. Interrupting replaces
104
+ the active Python worker, so the source remains rerunnable but live runtime
105
+ state is deliberately discarded. It also cancels that session's in-flight
106
+ Pydantic AI or LangChain model and async tool work, without cancelling another
107
+ session. Cancellation does not roll back completed effects or forcibly stop
108
+ arbitrary synchronous tool code running in a server thread.
109
+
110
+ Embedded-Python bridge operations and terminal execution have 120-second
111
+ timeouts; this is not a universal deadline for a complete model-backed cell.
112
+ Package installation has a 10-minute limit. Idle sessions expire after 30 minutes.
113
+
114
+ Notebook execution is non-transactional. Rerunning a cell is a new execution
115
+ attempt against current state; it does not roll back previous side effects.
116
+ Displayed cell numbers follow notebook order and remain unchanged across
117
+ reruns. Adding, moving, or deleting cells recomputes affected positions.
118
+
119
+ Notebook files are validated before they replace the current document. The UI
120
+ warns before discarding unsaved changes and limits files to 5 MB, notebooks to
121
+ 1,000 cells, individual cell source to 1 MB, and retained inline output to
122
+ 200,000 characters. Output beyond that boundary is marked as truncated.
123
+
124
+ Downloading asks which form to save. **Just notebook** writes cell sources,
125
+ types, visibility, and layout without outputs or environment values. **Save
126
+ progress** also writes retained cell outputs/results and the strict,
127
+ pickle-free `InteractiveSession` snapshot containing the current KediEnv. It
128
+ never includes Secret Manager or process environment values. Opening a progress
129
+ file restores the Kedi session on the next execution. If any live value cannot
130
+ be represented without changing its semantics, progress download is rejected
131
+ instead of producing a partial snapshot.
132
+
133
+ ## Editing
134
+
135
+ Kedi cells provide Tree-sitter highlighting, Kedi and embedded-Python
136
+ completion, hover, references, rename, live diagnostics, signature help,
137
+ definition navigation, and runtime error markers. Rename is limited to the
138
+ active cell when its LSP context includes earlier executed cells, avoiding a
139
+ silent partial edit. Monaco falls back to a plain multiline editor if its external assets
140
+ cannot be loaded. Markdown cells safely render headings, paragraphs, lists,
141
+ fenced code, blockquotes, emphasis, links, and separators; raw HTML is not
142
+ executed.
143
+
144
+ Use `Shift+Enter` to run the active cell, `Ctrl/Cmd+S` to download the notebook,
145
+ `Ctrl/Cmd+Enter` to insert after the active cell, `Ctrl/Cmd+Shift+Enter` to
146
+ append, `Ctrl/Cmd+Backspace` to delete, and `Ctrl/Cmd+ArrowUp/ArrowDown` to
147
+ move a cell. Toolbar actions expose the same operations.
148
+
149
+ Monaco is currently pinned on jsDelivr. Pyodide, its standard library, Micropip,
150
+ and Pydantic are vendored in the notebook wheel, so browser Python can start
151
+ without downloading its core runtime from a CDN. Packages installed later with
152
+ Micropip may still need network access. Host execution remains usable through
153
+ the fallback editor if Monaco is unavailable.
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.33.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kedi-notebook"
7
+ version = "0.1.0"
8
+ description = "Local interactive notebook for the Kedi Programming Language"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ dependencies = [
13
+ "basedpyright>=1.40.2,<1.41",
14
+ "fastapi>=0.162.0,<0.163",
15
+ "kedi>=0.1.2,<0.2",
16
+ "python-dotenv>=1.2.4,<2",
17
+ "uvicorn>=0.54,<0.55",
18
+ ]
19
+
20
+ [project.urls]
21
+ Source = "https://github.com/kedi-lang/notebook"
22
+ Tracker = "https://github.com/kedi-lang/notebook/issues"
23
+
24
+ [project.scripts]
25
+ kedi-notebook = "kedi_notebook.server:main"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "httpx==0.28.1",
30
+ "playwright==1.64.1",
31
+ "pytest==9.2.0",
32
+ "pytest-cov==7.1.0",
33
+ "ruff==0.17.4",
34
+ ]
35
+
36
+ [tool.hatch.build]
37
+ include = [
38
+ "src/kedi_notebook/**/*.py",
39
+ "src/kedi_notebook/static/**",
40
+ ]
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/kedi_notebook"]
44
+
45
+ [tool.uv.sources]
46
+ kedi = { path = "..", editable = true }
47
+
48
+ [tool.ruff]
49
+ line-length = 100
50
+ target-version = "py311"
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "UP"]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+
58
+ [tool.coverage.run]
59
+ branch = true
60
+ source = ["kedi_notebook"]
61
+
62
+ [tool.coverage.report]
63
+ show_missing = true
@@ -0,0 +1,15 @@
1
+ """Local interactive notebook support for Kedi."""
2
+
3
+ from .runtime import (
4
+ HostPython,
5
+ NotebookSessionManager,
6
+ discover_host_pythons,
7
+ )
8
+ from .secrets import NotebookSecretStore
9
+
10
+ __all__ = [
11
+ "HostPython",
12
+ "NotebookSecretStore",
13
+ "NotebookSessionManager",
14
+ "discover_host_pythons",
15
+ ]
@@ -0,0 +1,4 @@
1
+ from .server import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections import deque
5
+ from collections.abc import Iterator, Mapping
6
+ from threading import Condition, Lock
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ _MAX_OUTPUT_CHARS = 200_000
11
+
12
+
13
+ class BridgeCancelled(RuntimeError):
14
+ pass
15
+
16
+
17
+ class BridgeRun:
18
+ def __init__(self, run_id: str) -> None:
19
+ self.run_id = run_id
20
+ self.condition = Condition()
21
+ self.pending: deque[dict[str, Any]] = deque()
22
+ self.waiting: set[str] = set()
23
+ self.responses: dict[str, dict[str, Any]] = {}
24
+ self.output_events: dict[str, deque[dict[str, str]]] = {}
25
+ self.output_sizes: dict[str, int] = {}
26
+ self.done = False
27
+ self.cancelled = False
28
+ self.updated_at = time.monotonic()
29
+
30
+ def request(self, payload: Mapping[str, Any], *, timeout: float) -> Mapping[str, Any]:
31
+ response: Mapping[str, Any] | None = None
32
+ for event in self.request_events(payload, timeout=timeout):
33
+ if event["type"] == "response":
34
+ response = event["response"]
35
+ if response is None:
36
+ raise RuntimeError("Playground bridge request returned no response")
37
+ return response
38
+
39
+ def request_events(
40
+ self,
41
+ payload: Mapping[str, Any],
42
+ *,
43
+ timeout: float,
44
+ ) -> Iterator[dict[str, Any]]:
45
+ request_id = uuid4().hex
46
+ request = {"id": request_id, **dict(payload)}
47
+ with self.condition:
48
+ self._assert_open()
49
+ self.pending.append(request)
50
+ self.waiting.add(request_id)
51
+ self.output_events[request_id] = deque()
52
+ self.output_sizes[request_id] = 0
53
+ self.updated_at = time.monotonic()
54
+ self.condition.notify_all()
55
+ deadline = time.monotonic() + timeout
56
+ completed = False
57
+ try:
58
+ while True:
59
+ with self.condition:
60
+ while not self.output_events[request_id] and request_id not in self.responses:
61
+ self._assert_open()
62
+ remaining = deadline - time.monotonic()
63
+ if remaining <= 0:
64
+ raise TimeoutError("Timed out waiting for the browser model")
65
+ self.condition.wait(timeout=min(remaining, 1))
66
+ if self.output_events[request_id]:
67
+ event: dict[str, Any] = self.output_events[request_id].popleft()
68
+ else:
69
+ event = {
70
+ "type": "response",
71
+ "response": self.responses.pop(request_id),
72
+ }
73
+ completed = True
74
+ yield event
75
+ if completed:
76
+ return
77
+ finally:
78
+ with self.condition:
79
+ self.waiting.discard(request_id)
80
+ self.responses.pop(request_id, None)
81
+ self.output_events.pop(request_id, None)
82
+ self.output_sizes.pop(request_id, None)
83
+
84
+ def next_request(self, *, timeout: float) -> dict[str, Any] | None:
85
+ with self.condition:
86
+ deadline = time.monotonic() + timeout
87
+ while not self.pending and not self.done and not self.cancelled:
88
+ remaining = deadline - time.monotonic()
89
+ if remaining <= 0:
90
+ break
91
+ self.condition.wait(timeout=min(remaining, 1))
92
+ self.updated_at = time.monotonic()
93
+ return self.pending.popleft() if self.pending else None
94
+
95
+ def submit_response(self, request_id: str, response: Mapping[str, Any]) -> None:
96
+ with self.condition:
97
+ if request_id not in self.waiting:
98
+ raise ValueError("Response does not belong to an active bridge request")
99
+ if request_id in self.responses:
100
+ raise ValueError("Duplicate bridge response")
101
+ self.responses[request_id] = dict(response)
102
+ self.updated_at = time.monotonic()
103
+ self.condition.notify_all()
104
+
105
+ def submit_output(self, request_id: str, *, stream: str, text: str) -> None:
106
+ with self.condition:
107
+ if request_id not in self.waiting:
108
+ raise ValueError("Output does not belong to an active bridge request")
109
+ if stream not in {"stdout", "stderr"}:
110
+ raise ValueError("Bridge output stream must be stdout or stderr")
111
+ if not text:
112
+ return
113
+ remaining = _MAX_OUTPUT_CHARS - self.output_sizes[request_id]
114
+ if remaining <= 0:
115
+ return
116
+ text = text[:remaining]
117
+ self.output_sizes[request_id] += len(text)
118
+ self.output_events[request_id].append(
119
+ {"type": "output", "stream": stream, "text": text}
120
+ )
121
+ self.updated_at = time.monotonic()
122
+ self.condition.notify_all()
123
+
124
+ def finish(self) -> None:
125
+ with self.condition:
126
+ self.done = True
127
+ self.updated_at = time.monotonic()
128
+ self.condition.notify_all()
129
+
130
+ def cancel(self) -> None:
131
+ with self.condition:
132
+ self.cancelled = True
133
+ self.updated_at = time.monotonic()
134
+ self.condition.notify_all()
135
+
136
+ def _assert_open(self) -> None:
137
+ if self.cancelled:
138
+ raise BridgeCancelled("Playground run was cancelled")
139
+ if self.done:
140
+ raise RuntimeError("Playground bridge run is already complete")
141
+
142
+
143
+ class BridgeManager:
144
+ def __init__(self) -> None:
145
+ self._runs: dict[str, BridgeRun] = {}
146
+ self._lock = Lock()
147
+
148
+ def get_or_create(self, run_id: str) -> BridgeRun:
149
+ with self._lock:
150
+ run = self._runs.get(run_id)
151
+ if run is None:
152
+ run = BridgeRun(run_id)
153
+ self._runs[run_id] = run
154
+ return run
155
+
156
+ def cancel(self, run_id: str) -> None:
157
+ self.get_or_create(run_id).cancel()
158
+
159
+ def cleanup(self, *, max_age: float = 600) -> None:
160
+ cutoff = time.monotonic() - max_age
161
+ with self._lock:
162
+ stale = [
163
+ run_id
164
+ for run_id, run in self._runs.items()
165
+ if run.updated_at < cutoff and (run.done or run.cancelled)
166
+ ]
167
+ for run_id in stale:
168
+ del self._runs[run_id]
169
+
170
+
171
+ __all__ = ["BridgeCancelled", "BridgeManager", "BridgeRun"]