From 0bd04a825d555f3b31164321aa424ed377126d39 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:33:04 +0800 Subject: [PATCH] update: optimize config.py --- opencode_bridge/__init__.py | 28 +++++++- opencode_bridge/config.py | 124 ++++++++++++++++++++++++++++++++---- 2 files changed, 140 insertions(+), 12 deletions(-) diff --git a/opencode_bridge/__init__.py b/opencode_bridge/__init__.py index 773a605..079df3e 100644 --- a/opencode_bridge/__init__.py +++ b/opencode_bridge/__init__.py @@ -3,12 +3,24 @@ try: except ImportError: # Fallback when using the package in dev mode without installing # in editable mode with pip. It is highly recommended to install - # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs + # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-install/ import warnings warnings.warn("Importing 'opencode_bridge' outside a proper installation.") __version__ = "dev" +from .config import OpencodeConfig from .routes import setup_route_handlers +# Re-export OpencodeConfig at module level so the JupyterApp CLI parser +# (which auto-discovers Configurable subclasses from extension-point +# modules) recognises ``--OpencodeConfig.`` flags. +__all__ = [ + "OpencodeConfig", + "setup_route_handlers", + "_jupyter_labextension_paths", + "_jupyter_server_extension_points", + "_load_jupyter_server_extension", +] + def _jupyter_labextension_paths(): return [{ @@ -26,11 +38,25 @@ def _jupyter_server_extension_points(): def _load_jupyter_server_extension(server_app): """Registers the API handler to receive HTTP requests from the frontend extension. + Also instantiates the :class:`OpencodeConfig` configurable (with the + server app as parent so CLI / config-file overrides take effect) and + exposes it to request handlers via + ``web_app.settings["opencode_bridge"]``. + Parameters ---------- server_app: jupyterlab.labapp.LabApp JupyterLab application instance """ + # Belt-and-suspenders: ensure the CLI parser recognises --OpencodeConfig.* + # even on JupyterApp versions that don't auto-discover Configurable + # subclasses from extension-point modules. + if OpencodeConfig not in server_app.classes: + server_app.classes.append(OpencodeConfig) + + cfg = OpencodeConfig(parent=server_app) + server_app.web_app.settings["opencode_bridge"] = cfg + setup_route_handlers(server_app.web_app) name = "opencode_bridge" server_app.log.info(f"Registered {name} server extension") diff --git a/opencode_bridge/config.py b/opencode_bridge/config.py index 3267ec4..0c62f95 100644 --- a/opencode_bridge/config.py +++ b/opencode_bridge/config.py @@ -1,10 +1,22 @@ """Configuration resolution for the opencode-bridge extension. -All connection fields (URL / user / password / request timeout) are resolved -exclusively from environment variables + built-in defaults at server startup. -They are NOT stored in the JupyterLab plugin settings. +Connection fields (URL / user / password / request timeout) are resolved +in the following priority order, highest first: + +1. ``--OpencodeConfig.`` CLI flag (e.g. ``--OpencodeConfig.url=...``) +2. ``c.OpencodeConfig.`` line in a Jupyter config file +3. ``OPENCODE_BRIDGE_*`` environment variable +4. Built-in default + +The ``OpencodeConfig`` class is a ``traitlets.config.Configurable``; the +Jupyter server extension instantiates it with ``parent=server_app`` and +stashes it on ``web_app.settings["opencode_bridge"]`` so handlers can pick +it up via ``resolve_config``. + +Env vars (kept for backward compatibility — also the default-value source +for the traitlets so a fresh ``OpencodeConfig()`` with no overrides still +honours them): -Env vars: OPENCODE_BRIDGE_URL — OpenCode Serve base URL (default http://127.0.0.1:4096) OPENCODE_BRIDGE_USER — HTTP Basic Auth username (default 'opencode') OPENCODE_BRIDGE_PASSWORD — HTTP Basic Auth password (default '' = no auth) @@ -15,6 +27,9 @@ from __future__ import annotations import os from typing import NamedTuple, Optional, Tuple +from traitlets import Int, Unicode, default +from traitlets.config import Configurable + ENV_URL = "OPENCODE_BRIDGE_URL" ENV_USER = "OPENCODE_BRIDGE_USER" @@ -40,19 +55,106 @@ class OpenCodeConfig(NamedTuple): return (self.user, self.password) -def resolve_config(jupyter_settings: dict) -> OpenCodeConfig: - """Resolve OpenCode connection config from environment variables + defaults. +class OpencodeConfig(Configurable): + """Traitlets-configurable connection settings for the opencode-bridge server extension. - The `jupyter_settings` argument is accepted for API compatibility but no - fields are read from it — all connection config is now startup-only - (environment variables). + Exposed to ``jupyter lab`` / ``jupyter server`` as ``--OpencodeConfig.`` + and to ``jupyter_server_config.py`` as ``c.OpencodeConfig.``. Defaults + fall back to the ``OPENCODE_BRIDGE_*`` env vars and then to built-in defaults, + so the legacy env-var-only flow keeps working unchanged. """ - _ = jupyter_settings # intentionally unused; see module docstring + + url = Unicode( + DEFAULT_URL, + config=True, + help=( + "Base URL of the locally-running OpenCode Serve process. " + "Overrides the OPENCODE_BRIDGE_URL env var." + ), + ) + + user = Unicode( + DEFAULT_USER, + config=True, + help=( + "HTTP Basic Auth username for OpenCode Serve. " + "Overrides the OPENCODE_BRIDGE_USER env var." + ), + ) + + password = Unicode( + "", + config=True, + help=( + "HTTP Basic Auth password for OpenCode Serve (empty = no auth). " + "Overrides the OPENCODE_BRIDGE_PASSWORD env var." + ), + ) + + request_timeout_seconds = Int( + DEFAULT_REQUEST_TIMEOUT, + config=True, + help=( + "Per-request timeout in seconds when calling OpenCode Serve. " + "Overrides the OPENCODE_BRIDGE_TIMEOUT env var." + ), + ) + + @default("url") + def _default_url(self) -> str: + return os.environ.get(ENV_URL) or DEFAULT_URL + + @default("user") + def _default_user(self) -> str: + return os.environ.get(ENV_USER) or DEFAULT_USER + + @default("password") + def _default_password(self) -> str: + return os.environ.get(ENV_PASSWORD) or "" + + @default("request_timeout_seconds") + def _default_request_timeout_seconds(self) -> int: + return int(os.environ.get(ENV_TIMEOUT) or DEFAULT_REQUEST_TIMEOUT) + + +def _to_opencode_config(bridge) -> OpenCodeConfig: + """Project an :class:`OpencodeConfig` (or duck-typed equivalent) to the + internal ``OpenCodeConfig`` NamedTuple consumed by ``OpenCodeClient``. + """ + return OpenCodeConfig( + url=bridge.url, + user=bridge.user, + password=bridge.password, + request_timeout_seconds=bridge.request_timeout_seconds, + ) + + +def resolve_config(jupyter_settings) -> OpenCodeConfig: + """Resolve OpenCode connection config into an ``OpenCodeConfig`` NamedTuple. + + Accepts either a settings dict (legacy) or an :class:`OpencodeConfig` + instance directly, because request handlers call + ``resolve_config(handler.settings.get("opencode_bridge", {}))`` and pass + the inner value (which is now an ``OpencodeConfig`` instance after the + server extension's load hook populates it). + + Source order (highest priority first): + + 1. The argument itself is an :class:`OpencodeConfig` instance. + 2. ``jupyter_settings["opencode_bridge"]`` is an :class:`OpencodeConfig` + instance (legacy call style). + 3. Otherwise fall back to env vars + built-in defaults. + """ + if isinstance(jupyter_settings, OpencodeConfig): + return _to_opencode_config(jupyter_settings) + bridge = (jupyter_settings or {}).get("opencode_bridge") + if isinstance(bridge, OpencodeConfig): + return _to_opencode_config(bridge) return OpenCodeConfig( url=os.environ.get(ENV_URL) or DEFAULT_URL, user=os.environ.get(ENV_USER) or DEFAULT_USER, password=os.environ.get(ENV_PASSWORD) or "", request_timeout_seconds=int( os.environ.get(ENV_TIMEOUT) or DEFAULT_REQUEST_TIMEOUT - ) + ), )