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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ pathview_server/ # Python package (pip install pathview)
├── cli.py # CLI entry point (pathview serve)
└── static/ # Bundled frontend (generated at build time)


scripts/
├── config/ # Configuration files for extraction
│ ├── schemas/ # JSON schemas for validation
Expand Down Expand Up @@ -496,8 +497,9 @@ npm run dev # Starts Vite dev server (separate terminal)

**Key properties:**
- **Process isolation** — each session gets its own Python subprocess
- **Host environment** — workers run with the same Python used to install pathview, so all packages in the user's environment are available in the code editor
- **Namespace persistence** — variables persist across exec/eval calls within a session
- **Dynamic packages** — packages from `PYTHON_PACKAGES` (the same config used by Pyodide) are pip-installed on first init
- **Dynamic packages** — packages from `PYTHON_PACKAGES` (the same config used by Pyodide) are pip-installed on first init if not already present
- **Session TTL** — stale sessions cleaned up after 1 hour of inactivity
- **Streaming** — simulations stream via SSE, with the same code injection support as Pyodide

Expand Down
4 changes: 1 addition & 3 deletions pathview_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS

from pathview_server.venv import get_venv_python

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
Expand All @@ -44,7 +42,7 @@ def __init__(self, session_id: str):
self.last_active = time.time()
self.lock = threading.Lock()
self.process = subprocess.Popen(
[get_venv_python(), "-u", WORKER_SCRIPT],
[sys.executable, "-u", WORKER_SCRIPT],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
Expand Down
5 changes: 1 addition & 4 deletions pathview_server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import webbrowser

from pathview_server import __version__
from pathview_server.venv import VENV_DIR, ensure_venv


def main():
Expand All @@ -30,8 +29,6 @@ def main():

args = parser.parse_args()

ensure_venv()

from pathview_server.app import create_app

app = create_app(serve_static=not args.debug)
Expand All @@ -52,7 +49,7 @@ def open_browser_when_ready():
threading.Thread(target=open_browser_when_ready, daemon=True).start()

print(f"PathView v{__version__}")
print(f" Python venv: {VENV_DIR}")
print(f" Python: {sys.executable}")
print(f"Running at http://{args.host}:{args.port}")

if args.host == "0.0.0.0":
Expand Down
34 changes: 0 additions & 34 deletions pathview_server/venv.py

This file was deleted.

1 change: 0 additions & 1 deletion pathview_server/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ def initialize(packages: list[dict] | None = None) -> None:
send({"type": "stderr", "value": f"Optional package {pkg.get('import', '?')} failed: {e}\n"})

# Import numpy AFTER packages are installed (numpy comes with pathsim).
# In a fresh venv without simulation packages, numpy won't be available.
try:
exec("import numpy as np", _namespace)
except Exception:
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies = [
"flask-cors>=4.0",
"numpy",
"waitress>=3.0",
"pathsim",
"pathsim-chem==0.2rc3",
]

[project.optional-dependencies]
Expand Down
62 changes: 61 additions & 1 deletion scripts/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,10 @@ def extract(self) -> dict:
class DependencyExtractor:
"""Extract and generate dependency configuration."""

# Packages managed by the extraction script in pyproject.toml.
# These are added/updated automatically; all other dependencies are left untouched.
MANAGED_PACKAGE_NAMES = {"pathsim", "pathsim-chem"}

def __init__(self, config_loader: ConfigLoader):
self.config = config_loader

Expand Down Expand Up @@ -735,6 +739,59 @@ def extract(self) -> dict:
"extracted_versions": extracted_versions
}

def update_pyproject(self, packages: list[dict], project_root: Path) -> None:
"""Update pyproject.toml dependencies with pinned package versions.

Adds/updates entries for packages from requirements-pyodide.txt
while leaving all other dependencies (flask, numpy, etc.) untouched.
"""
pyproject_path = project_root / "pyproject.toml"
if not pyproject_path.exists():
print(" Warning: pyproject.toml not found, skipping")
return

text = pyproject_path.read_text(encoding="utf-8")

# Build pip specs for managed packages (e.g. "pathsim==0.17.0")
new_specs: list[str] = []
for pkg in packages:
base_name = pkg["pip"].split(">=")[0].split("==")[0].split("<=")[0].split("<")[0].split(">")[0]
if base_name in self.MANAGED_PACKAGE_NAMES:
new_specs.append(pkg["pip"])

# Parse the existing dependencies list
pattern = re.compile(
r'(dependencies\s*=\s*\[)(.*?)(\])',
re.DOTALL,
)
match = pattern.search(text)
if not match:
print(" Warning: Could not find dependencies in pyproject.toml, skipping")
return

existing_block = match.group(2)

# Keep non-managed dependencies
kept: list[str] = []
for line in existing_block.strip().splitlines():
dep = line.strip().strip(",").strip('"').strip("'")
if not dep:
continue
dep_name = re.split(r'[><=~!\[]', dep)[0].strip()
if dep_name not in self.MANAGED_PACKAGE_NAMES:
kept.append(dep)

# Append managed packages
all_deps = kept + new_specs

# Rebuild the dependencies block
dep_lines = ",\n".join(f' "{d}"' for d in all_deps)
new_block = f"dependencies = [\n{dep_lines},\n]"
text = text[:match.start()] + new_block + text[match.end():]

pyproject_path.write_text(text, encoding="utf-8")
print(f" Updated pyproject.toml dependencies: {', '.join(new_specs)}")


# =============================================================================
# TypeScript Generation
Expand Down Expand Up @@ -1009,8 +1066,11 @@ def main():

if extract_all or args.deps:
print("\nExtracting dependencies...")
deps = DependencyExtractor(config).extract()
dep_extractor = DependencyExtractor(config)
deps = dep_extractor.extract()
generator.write_dependencies(deps)
project_root = Path(__file__).parent.parent
dep_extractor.update_pyproject(deps["packages"], project_root)

# Track extracted data for registry generation
blocks = None
Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Auto-generated by scripts/extract.py - DO NOT EDIT
// Source: scripts/config/requirements-pyodide.txt, scripts/config/pyodide.json

export const PATHVIEW_VERSION = '0.6.1';
export const PATHVIEW_VERSION = '0.7.0';

export const PYODIDE_VERSION = '0.26.2';
export const PYODIDE_CDN_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.mjs`;
Expand Down
2 changes: 0 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import pytest

from pathview_server.app import create_app, _sessions, _sessions_lock
from pathview_server.venv import ensure_venv


@pytest.fixture()
def app():
"""Create a Flask test app (API-only, no static serving)."""
ensure_venv()
application = create_app(serve_static=False)
application.config["TESTING"] = True
yield application
Expand Down