From e5ba3412f29d54148932476492e7c6b4cf59b08e Mon Sep 17 00:00:00 2001 From: haikdc Date: Sun, 5 Apr 2026 21:17:03 -0700 Subject: [PATCH] [Haik]: classes now extend basemodel and private vars properly dont start with an underscore --- .../subscriptions/NineRouter/NineRouter.py | 47 +++++++++---------- .../NineRouterClient/NineRouterClient.py | 22 ++++----- .../NineRouterProcess/NineRouterProcess.py | 40 ++++++++-------- 3 files changed, 54 insertions(+), 55 deletions(-) diff --git a/backend/apps/subscriptions/NineRouter/NineRouter.py b/backend/apps/subscriptions/NineRouter/NineRouter.py index d29993f0..6352e5d9 100644 --- a/backend/apps/subscriptions/NineRouter/NineRouter.py +++ b/backend/apps/subscriptions/NineRouter/NineRouter.py @@ -8,66 +8,65 @@ ensure-task, HTTP client) lives in one place. import asyncio from typing import ClassVar, Optional +from pydantic import BaseModel, Field from typeguard import typechecked from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.NineRouterProcess import NineRouterProcess from backend.apps.subscriptions.NineRouter.helpers.NineRouterClient.NineRouterClient import NineRouterClient -class NineRouter: - _instance: ClassVar[Optional["NineRouter"]] = None - - def __init__(self) -> None: - self._process: NineRouterProcess = NineRouterProcess() - self._client: NineRouterClient = NineRouterClient() - self._ensure_task: Optional[asyncio.Task] = None +class NineRouter(BaseModel): + p_instance: ClassVar[Optional["NineRouter"]] = None + p_process: NineRouterProcess = Field(default_factory=NineRouterProcess) + p_client: NineRouterClient = Field(default_factory=NineRouterClient) + p_ensure_task: Optional[asyncio.Task] = None @classmethod def get(cls) -> "NineRouter": - if cls._instance is None: - cls._instance = cls() - return cls._instance + if cls.p_instance is None: + cls.p_instance = cls() + return cls.p_instance # -- lifecycle ------------------------------------------------------------- @typechecked def is_running(self) -> bool: - return self._process.is_running() + return self.p_process.is_running() @typechecked async def ensure_running(self) -> None: - await self._process.ensure_running() + await self.p_process.ensure_running() @typechecked async def stop(self) -> None: self.cancel_ensure_task() - await self._client.aclose() - self._process.stop() + await self.p_client.aclose() + self.p_process.stop() @typechecked async def ensure_running_background(self) -> None: """Kick off ensure_running as a background task if not already in flight.""" - if self._ensure_task is None or self._ensure_task.done(): - self._ensure_task = asyncio.create_task(self._process.ensure_running()) + if self.p_ensure_task is None or self.p_ensure_task.done(): + self.p_ensure_task = asyncio.create_task(self.p_process.ensure_running()) @typechecked def cancel_ensure_task(self) -> None: - if self._ensure_task and not self._ensure_task.done(): - self._ensure_task.cancel() + if self.p_ensure_task and not self.p_ensure_task.done(): + self.p_ensure_task.cancel() # -- client ---------------------------------------------------------------- @typechecked async def get_providers(self) -> list[dict] | dict: - return await self._client.get_providers() + return await self.p_client.get_providers() @typechecked async def get_models(self) -> list[dict]: - return await self._client.get_models() + return await self.p_client.get_models() @typechecked async def start_oauth(self, provider: str) -> dict: - return await self._client.start_oauth(provider) + return await self.p_client.start_oauth(provider) @typechecked async def poll_oauth( @@ -77,7 +76,7 @@ class NineRouter: code_verifier: str | None = None, extra_data: dict | None = None, ) -> dict: - return await self._client.poll_oauth(provider, device_code, code_verifier=code_verifier, extra_data=extra_data) + return await self.p_client.poll_oauth(provider, device_code, code_verifier=code_verifier, extra_data=extra_data) @typechecked async def exchange_oauth( @@ -88,8 +87,8 @@ class NineRouter: code_verifier: str, state: str = "", ) -> dict: - return await self._client.exchange_oauth(provider, code, redirect_uri, code_verifier, state) + return await self.p_client.exchange_oauth(provider, code, redirect_uri, code_verifier, state) @typechecked async def disconnect_provider(self, provider_id: str) -> bool: - return await self._client.disconnect_provider(provider_id) + return await self.p_client.disconnect_provider(provider_id) diff --git a/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py b/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py index 7cc66cb9..05faacfd 100644 --- a/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py +++ b/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py @@ -1,23 +1,23 @@ """HTTP client for 9Router's REST API.""" import httpx +from pydantic import Field, BaseModel from typeguard import typechecked from backend.apps.subscriptions.NineRouter.helpers.constants import NINE_ROUTER_API, NINE_ROUTER_V1 from backend.ports import NINE_ROUTER_PORT -class NineRouterClient: - def __init__(self) -> None: - self._http: httpx.AsyncClient = httpx.AsyncClient(timeout=15.0) +class NineRouterClient(BaseModel): + p_http: httpx.AsyncClient = Field(default_factory=httpx.AsyncClient(timeout=15.0)) async def aclose(self) -> None: - await self._http.aclose() + await self.p_http.aclose() @typechecked async def get_providers(self) -> list[dict] | dict: try: - r = await self._http.get(f"{NINE_ROUTER_API}/providers", timeout=5.0) + r = await self.p_http.get(f"{NINE_ROUTER_API}/providers", timeout=5.0) if r.status_code == 200: return r.json() except Exception as e: @@ -32,7 +32,7 @@ class NineRouterClient: authorization_code providers: returns {authUrl, codeVerifier, state} """ try: - r = await self._http.get(f"{NINE_ROUTER_API}/oauth/{provider}/device-code") + r = await self.p_http.get(f"{NINE_ROUTER_API}/oauth/{provider}/device-code") if r.status_code == 200: data: dict = r.json() return { @@ -47,7 +47,7 @@ class NineRouterClient: pass callback_url: str = f"http://localhost:{NINE_ROUTER_PORT}/callback" - r = await self._http.get( + r = await self.p_http.get( f"{NINE_ROUTER_API}/oauth/{provider}/authorize", params={"redirect_uri": callback_url}, ) @@ -75,7 +75,7 @@ class NineRouterClient: if extra_data: body["extraData"] = extra_data - r = await self._http.post(f"{NINE_ROUTER_API}/oauth/{provider}/poll", json=body) + r = await self.p_http.post(f"{NINE_ROUTER_API}/oauth/{provider}/poll", json=body) r.raise_for_status() return r.json() @@ -95,7 +95,7 @@ class NineRouterClient: "state": state, } print(f"exchange_oauth: provider={provider} redirect_uri={redirect_uri}") - r = await self._http.post(f"{NINE_ROUTER_API}/oauth/{provider}/exchange", json=payload) + r = await self.p_http.post(f"{NINE_ROUTER_API}/oauth/{provider}/exchange", json=payload) print(f"exchange_oauth: status={r.status_code}") r.raise_for_status() return r.json() @@ -103,7 +103,7 @@ class NineRouterClient: @typechecked async def get_models(self) -> list[dict]: try: - r = await self._http.get(f"{NINE_ROUTER_V1}/models", timeout=5.0) + r = await self.p_http.get(f"{NINE_ROUTER_V1}/models", timeout=5.0) if r.status_code == 200: data: dict = r.json() models: list = data.get("data", []) @@ -122,5 +122,5 @@ class NineRouterClient: @typechecked async def disconnect_provider(self, provider_id: str) -> bool: - r = await self._http.delete(f"{NINE_ROUTER_API}/providers/{provider_id}", timeout=10.0) + r = await self.p_http.delete(f"{NINE_ROUTER_API}/providers/{provider_id}", timeout=10.0) return r.status_code == 200 diff --git a/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/NineRouterProcess.py b/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/NineRouterProcess.py index 426c7a1b..dbef3a92 100644 --- a/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/NineRouterProcess.py +++ b/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/NineRouterProcess.py @@ -12,6 +12,7 @@ import threading from typing import Optional import httpx +from pydantic import Field, BaseModel from typeguard import typechecked from backend.ports import NINE_ROUTER_PORT @@ -20,12 +21,11 @@ from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.helpers.for from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.helpers.find_9router_dir import find_9router_dir from backend.apps.subscriptions.NineRouter.helpers.NineRouterProcess.helpers.find_node import find_node -_THIS_DIR: str = os.path.dirname(os.path.abspath(__file__)) +P_THIS_DIR: str = os.path.dirname(os.path.abspath(__file__)) -class NineRouterProcess: - def __init__(self) -> None: - self._process: Optional[subprocess.Popen] = None +class NineRouterProcess(BaseModel): + p_process: Optional[subprocess.Popen] = Field(default=None) @typechecked def is_running(self) -> bool: @@ -58,17 +58,17 @@ class NineRouterProcess: else: return - _9router_dir: Optional[str] = find_9router_dir(_THIS_DIR) + p_9router_dir: Optional[str] = find_9router_dir(P_THIS_DIR) cmd: list[str] cwd: str | None env: dict[str, str] - if _is_packaged and _9router_dir: - standalone_server: str = os.path.join(_9router_dir, "server.js") + if _is_packaged and p_9router_dir: + standalone_server: str = os.path.join(p_9router_dir, "server.js") if not os.path.exists(standalone_server): - standalone_server = os.path.join(_9router_dir, ".next", "standalone", "server.js") + standalone_server = os.path.join(p_9router_dir, ".next", "standalone", "server.js") if not os.path.exists(standalone_server): - print("9Router: standalone build not found in", _9router_dir, flush=True) + print("9Router: standalone build not found in", p_9router_dir, flush=True) return node: Optional[str] = find_node() @@ -88,24 +88,24 @@ class NineRouterProcess: if node == os.environ.get("OPENSWARM_ELECTRON_PATH"): env["ELECTRON_RUN_AS_NODE"] = "1" - elif _9router_dir: + elif p_9router_dir: npx: str | None = shutil.which("npx") if not npx: print("9Router: npx not found, cannot auto-start", flush=True) return - if not os.path.isdir(os.path.join(_9router_dir, "node_modules")): + if not os.path.isdir(os.path.join(p_9router_dir, "node_modules")): print("9Router: installing dependencies...", flush=True) npm: str | None = shutil.which("npm") if npm: subprocess.run( - [npm, "install"], cwd=_9router_dir, + [npm, "install"], cwd=p_9router_dir, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120, ) print(f"9Router: starting (dev) on port {NINE_ROUTER_PORT}...", flush=True) cmd = [npx, "next", "dev", "--webpack", "-p", str(NINE_ROUTER_PORT)] - cwd = _9router_dir + cwd = p_9router_dir env = { **os.environ, "PORT": str(NINE_ROUTER_PORT), @@ -127,14 +127,14 @@ class NineRouterProcess: } try: - self._process = subprocess.Popen( + self.p_process = subprocess.Popen( cmd, cwd=cwd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, ) - threading.Thread(target=forward_output, args=(self._process.stdout,), daemon=True).start() + threading.Thread(target=forward_output, args=(self.p_process.stdout,), daemon=True).start() timeout: int = 20 if _is_packaged else 30 for _ in range(timeout * 2): @@ -149,14 +149,14 @@ class NineRouterProcess: @typechecked def stop(self) -> None: - if self._process: + if self.p_process: try: - self._process.terminate() - self._process.wait(timeout=5) + self.p_process.terminate() + self.p_process.wait(timeout=5) except Exception: try: - self._process.kill() + self.p_process.kill() except Exception: pass - self._process = None + self.p_process = None print("9Router stopped")