From e09622729c6521c01655818cb39e613d9872d681 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 25 Mar 2026 11:08:03 -0700 Subject: [PATCH] feat: add config module and vision router stub --- circuitforge_core/config/__init__.py | 3 +++ circuitforge_core/config/settings.py | 27 +++++++++++++++++++++++++++ circuitforge_core/vision/__init__.py | 3 +++ circuitforge_core/vision/router.py | 19 +++++++++++++++++++ tests/test_config.py | 27 +++++++++++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 circuitforge_core/config/__init__.py create mode 100644 circuitforge_core/config/settings.py create mode 100644 circuitforge_core/vision/__init__.py create mode 100644 circuitforge_core/vision/router.py create mode 100644 tests/test_config.py diff --git a/circuitforge_core/config/__init__.py b/circuitforge_core/config/__init__.py new file mode 100644 index 0000000..8a8a56b --- /dev/null +++ b/circuitforge_core/config/__init__.py @@ -0,0 +1,3 @@ +from .settings import require_env, load_env + +__all__ = ["require_env", "load_env"] diff --git a/circuitforge_core/config/settings.py b/circuitforge_core/config/settings.py new file mode 100644 index 0000000..d626db7 --- /dev/null +++ b/circuitforge_core/config/settings.py @@ -0,0 +1,27 @@ +"""Env validation and .env loader for CircuitForge products.""" +from __future__ import annotations +import os +from pathlib import Path + + +def require_env(key: str) -> str: + """Return env var value or raise EnvironmentError with clear message.""" + value = os.environ.get(key) + if not value: + raise EnvironmentError( + f"Required environment variable {key!r} is not set. " + f"Check your .env file." + ) + return value + + +def load_env(path: Path) -> None: + """Load key=value pairs from a .env file into os.environ. Skips missing files.""" + if not path.exists(): + return + for line in path.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + os.environ.setdefault(key.strip(), value.strip()) diff --git a/circuitforge_core/vision/__init__.py b/circuitforge_core/vision/__init__.py new file mode 100644 index 0000000..22e88f7 --- /dev/null +++ b/circuitforge_core/vision/__init__.py @@ -0,0 +1,3 @@ +from .router import VisionRouter + +__all__ = ["VisionRouter"] diff --git a/circuitforge_core/vision/router.py b/circuitforge_core/vision/router.py new file mode 100644 index 0000000..c0171ba --- /dev/null +++ b/circuitforge_core/vision/router.py @@ -0,0 +1,19 @@ +""" +Vision model router — stub until v0.2. +Supports: moondream2 (local) and Claude vision API (cloud). +""" +from __future__ import annotations + + +class VisionRouter: + """Routes image analysis requests to local or cloud vision models.""" + + def analyze(self, image_bytes: bytes, prompt: str) -> str: + """ + Analyze image_bytes with the given prompt. + Raises NotImplementedError until vision backends are wired up. + """ + raise NotImplementedError( + "VisionRouter is not yet implemented. " + "Photo analysis requires a Paid tier or local vision model (v0.2+)." + ) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..aaa0d80 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,27 @@ +import os +import pytest +from circuitforge_core.config import require_env, load_env + + +def test_require_env_returns_value_when_set(monkeypatch): + monkeypatch.setenv("TEST_KEY", "hello") + assert require_env("TEST_KEY") == "hello" + + +def test_require_env_raises_when_missing(monkeypatch): + monkeypatch.delenv("TEST_KEY", raising=False) + with pytest.raises(EnvironmentError, match="TEST_KEY"): + require_env("TEST_KEY") + + +def test_load_env_sets_variables(tmp_path, monkeypatch): + env_file = tmp_path / ".env" + env_file.write_text("FOO=bar\nBAZ=qux\n") + monkeypatch.delenv("FOO", raising=False) + load_env(env_file) + assert os.environ.get("FOO") == "bar" + assert os.environ.get("BAZ") == "qux" + + +def test_load_env_skips_missing_file(tmp_path): + load_env(tmp_path / "nonexistent.env") # must not raise