Skip to content
Draft
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: 2 additions & 2 deletions ccflow/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ def __panel__(self):
Requires ccflow UI dependencies (panel, panel_material_ui).
"""
try:
from ccflow.ui.model import ModelViewer
from ccflow.ui.panel.model import ModelViewer
except ImportError:
raise ImportError(
"panel and other optional dependencies must be installed to use ModelViewer. Pip install ccflow[full] to install all optional dependencies."
Expand Down Expand Up @@ -522,7 +522,7 @@ def __panel__(self):

Requires ccflow UI dependencies (panel, panel_material_ui).
"""
from ccflow.ui.registry import ModelRegistryViewer
from ccflow.ui.panel.registry import ModelRegistryViewer

return ModelRegistryViewer(self)

Expand Down
8 changes: 0 additions & 8 deletions ccflow/examples/tpch/config/conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,15 @@
# (``load_config(overrides=["tpch.backend.scale_factor=1.0"])``) reconfigures
# every table, answer and query consistently.

# ---------------------------------------------------------------------------
# Shared DuckDB backend. Plain ``ccflow.BaseModel`` — not callable itself,
# but registered so all providers share one connection and one ``dbgen`` call.
# ---------------------------------------------------------------------------
tpch:
backend:
_target_: ccflow.examples.tpch.TPCHDuckDBBackend
scale_factor: 0.1

# ---------------------------------------------------------------------------
# Per-table providers. One instance per TPC-H table; the output schema of
# each instance is fixed by its ``table`` field.
# ---------------------------------------------------------------------------
table:
customer:
_target_: ccflow.examples.tpch.TPCHTableProvider
Expand Down Expand Up @@ -71,10 +67,8 @@ table:
backend: /tpch/backend
table: supplier

# ---------------------------------------------------------------------------
# Reference answers, one per query, served straight from DuckDB's
# ``tpch_answers()`` table at the configured scale factor.
# ---------------------------------------------------------------------------
answer:
Q1:
_target_: ccflow.examples.tpch.TPCHAnswerProvider
Expand Down Expand Up @@ -165,12 +159,10 @@ answer:
backend: /tpch/backend
query_id: 22

# ---------------------------------------------------------------------------
# The 22 TPC-H queries. Each ``TPCHQuery`` is the same Python class with a
# different ``query_id`` and a different tuple of table-provider inputs.
# Wiring the inputs in YAML makes each query's table dependencies explicit
# and overridable per-query.
# ---------------------------------------------------------------------------
query:
Q1:
_target_: ccflow.examples.tpch.TPCHQuery
Expand Down
54 changes: 0 additions & 54 deletions ccflow/flow_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,6 @@
_AnyCallable = Callable[..., Any]


# ---------------------------------------------------------------------------
# Internal data structures
# ---------------------------------------------------------------------------


class _UnsetFlowInput:
def __repr__(self) -> str:
return "<unset>"
Expand Down Expand Up @@ -393,11 +388,6 @@ class _LocalFlowModelPicklePayload(NamedTuple):
factory_kwargs: dict[str, Any]


# ---------------------------------------------------------------------------
# Small value helpers
# ---------------------------------------------------------------------------


def _context_values(context: ContextBase) -> dict[str, Any]:
return dict(context)

Expand Down Expand Up @@ -469,11 +459,6 @@ def _concrete_context_type(context_type: Any) -> type[ContextBase] | None:
return None


# ---------------------------------------------------------------------------
# Type coercion, lazy thunks, and registry references
# ---------------------------------------------------------------------------


def _remember_type_adapter(cache: "OrderedDict[Any, Any]", key: Any, value: Any) -> Any:
cache[key] = value
cache.move_to_end(key)
Expand Down Expand Up @@ -670,11 +655,6 @@ def _ensure_named_python_function(fn: _AnyCallable, *, decorator_name: str) -> N
raise TypeError(f"{decorator_name} only supports named Python functions.")


# ---------------------------------------------------------------------------
# Context-transform serialization and generated-model persistence
# ---------------------------------------------------------------------------


def _serialize_context_transform_config(config: _FlowModelConfig) -> str:
payload = cloudpickle.dumps(_serialize_flow_model_config(config), protocol=5)
return b64encode(payload).decode("ascii")
Expand Down Expand Up @@ -867,11 +847,6 @@ def _register_generated_model_class(config: _FlowModelConfig, generated_cls: typ
)


# ---------------------------------------------------------------------------
# Runtime context contracts and dependency projection
# ---------------------------------------------------------------------------


def _runtime_context_for_model(model: CallableModel, values: dict[str, Any]) -> ContextBase:
"""Build the runtime context object expected by ``model`` from raw values."""

Expand Down Expand Up @@ -1026,11 +1001,6 @@ def _missing_regular_param_names(model: "_GeneratedFlowModelBase", config: _Flow
return missing


# ---------------------------------------------------------------------------
# Generated model input resolution
# ---------------------------------------------------------------------------


def _resolve_regular_param_value(model: "_GeneratedFlowModelBase", param: _FlowModelParam, context: ContextBase) -> Any:
value = getattr(model, param.name, _UNSET_FLOW_INPUT)
if _is_unset_flow_input(value):
Expand Down Expand Up @@ -1470,10 +1440,6 @@ def _coerce_model_context_value(model: CallableModel, field_name: str, value: An
return _coerce_value(field_name, value, contract.input_types[field_name], source)


# ---------------------------------------------------------------------------
# Effective identity helpers
# ---------------------------------------------------------------------------

# Identity terms used below:
# - config identity: stable hash of the analyzed Flow.model contract, fixed at
# generated-class construction time and carried through local restore.
Expand Down Expand Up @@ -1843,11 +1809,6 @@ def _generated_model_identity_payload(
)


# ---------------------------------------------------------------------------
# Static binding resolution and with_context normalization
# ---------------------------------------------------------------------------


def _resolved_static_contextual_values(
model: "_GeneratedFlowModelBase",
config: _FlowModelConfig,
Expand Down Expand Up @@ -2104,11 +2065,6 @@ def _normalize_with_context(model: CallableModel, patches: tuple[Any, ...], fiel
return _validate_static_context_spec_declared_context(model, context_spec)


# ---------------------------------------------------------------------------
# Bound context application and compute context construction
# ---------------------------------------------------------------------------


def _context_from_values_preserving_private_state(context: ContextBase, values: dict[str, Any]) -> ContextBase:
"""Validate updated public values while preserving private context state."""

Expand Down Expand Up @@ -2537,11 +2493,6 @@ def _recursive_dependency_specs_for_flow(
active.remove(model_id)


# ---------------------------------------------------------------------------
# model.flow API and BoundModel wrapper
# ---------------------------------------------------------------------------


class FlowAPI:
"""API namespace exposed as ``model.flow``.

Expand Down Expand Up @@ -3158,11 +3109,6 @@ def _evaluation_identity_payload(
return _generated_model_identity_payload(self, context)


# ---------------------------------------------------------------------------
# Generated model method builders and decorators
# ---------------------------------------------------------------------------


def _make_call_impl(config: _FlowModelConfig) -> _AnyCallable:
"""Create the ``__call__`` implementation for one generated model class."""

Expand Down
4 changes: 2 additions & 2 deletions ccflow/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ def test_widget(self):

def test_panel(self):
from ccflow import ModelRegistry
from ccflow.ui.model import ModelViewer
from ccflow.ui.registry import ModelRegistryViewer
from ccflow.ui.panel.model import ModelViewer
from ccflow.ui.panel.registry import ModelRegistryViewer

m = ModelA(x="foo")
panel_obj = m.__panel__()
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Unit tests for ccflow.ui.cli module."""
"""Unit tests for ccflow.ui.panel.cli module."""

from ccflow.ui.cli import _get_ui_args_parser
from ccflow.ui.panel.cli import _get_ui_args_parser


class TestGetUIArgsParser:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Unit tests for ccflow.ui.model module."""
"""Unit tests for ccflow.ui.panel.model module."""

import panel as pn
from pydantic import Field

from ccflow import BaseModel, CallableModel, ContextBase, Flow, GenericResult, MetaData, ModelRegistry
from ccflow.ui.model import ModelConfigViewer, ModelTypeViewer, ModelViewer
from ccflow.ui.panel.model import ModelConfigViewer, ModelTypeViewer, ModelViewer

from .utils import find_components_by_type

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Unit tests for ccflow.ui.registry module."""
"""Unit tests for ccflow.ui.panel.registry module."""

from unittest import mock

import panel as pn

from ccflow import BaseModel, ModelRegistry
from ccflow.ui.registry import ModelRegistryViewer, RegistryBrowser
from ccflow.ui.panel.registry import ModelRegistryViewer, RegistryBrowser

from .utils import find_components_by_type

Expand Down
File renamed without changes.
Empty file.
146 changes: 146 additions & 0 deletions ccflow/tests/ui/spaday/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Unit tests for ccflow.ui.spaday.cli module."""

import importlib
from pathlib import Path

import pytest
from spaday.bootstrap import _ASSETS, bundles_dir

from ccflow import BaseModel, LazyRegistry, ModelRegistry
from ccflow.ui.spaday.cli import _asset_layout, _get_ui_args_parser, serve_registry


class SimpleModel(BaseModel):
name: str
value: int = 0


class TestGetUIArgsParser:
def test_parser_composition(self):
parser = _get_ui_args_parser()
args = parser.parse_args([])

# From add_hydra_config_args
assert hasattr(args, "overrides")
assert hasattr(args, "config_path")
assert hasattr(args, "config_name")

# Server + viewer-specific
assert hasattr(args, "address")
assert hasattr(args, "port")
assert hasattr(args, "browser_width")
assert hasattr(args, "title")
assert hasattr(args, "sort_children")

def test_defaults(self):
args = _get_ui_args_parser().parse_args([])
assert args.address == "127.0.0.1"
assert args.port == 8080
assert args.browser_width == 400
assert args.title == "ccflow Model Registry"
assert args.sort_children is True

def test_custom_values(self):
args = _get_ui_args_parser().parse_args(["--address", "0.0.0.0", "--port", "9000", "--browser-width", "500", "--title", "Mine"])
assert args.address == "0.0.0.0"
assert args.port == 9000
assert args.browser_width == 500
assert args.title == "Mine"

def test_no_sort_children_flag(self):
args = _get_ui_args_parser().parse_args(["--no-sort-children"])
assert args.sort_children is False

def test_overrides_positional(self):
args = _get_ui_args_parser().parse_args(["key1=value1", "key2=value2"])
assert args.overrides == ["key1=value1", "key2=value2"]


class TestServeRegistry:
def test_builds_app_without_running(self):
registry = ModelRegistry(name="test")
registry.add("m", SimpleModel(name="m", value=1))
app = serve_registry(registry, run=False)
paths = {getattr(route, "path", None) for route in app.routes}
assert "/" in paths
assert "/tree.json" in paths

def test_tree_route_reflects_registry(self):
registry = ModelRegistry(name="test")
registry.add("widget", SimpleModel(name="widget"))
app = serve_registry(registry, title="T", run=False)
# The tree route serializes the viewer; the model path should appear in it.
tree_route = next(r for r in app.routes if getattr(r, "path", None) == "/tree.json")
assert tree_route is not None

def test_materialize_route_present(self):
registry = ModelRegistry(name="test")
registry.add("m", SimpleModel(name="m"))
app = serve_registry(registry, run=False)
paths = {getattr(route, "path", None) for route in app.routes}
assert "/materialize" in paths

@pytest.mark.parametrize("module", ["ccflow.ui.cli", "ccflow.ui.model", "ccflow.ui.registry"])
def test_panel_module_compatibility_imports(self, module):
assert importlib.import_module(module)


class TestMaterializeEndpoint:
def _lazy_registry(self):
return LazyRegistry(
name="root",
group={"model": {"_target_": "ccflow.tests.ui.spaday.test_cli.SimpleModel", "name": "pending"}},
)

def test_materialize_instantiates_pending_model(self, mocker):
starlette_testclient = pytest.importorskip("starlette.testclient")
from ccflow.ui.spaday import cli

to_thread = mocker.spy(cli.asyncio, "to_thread")
registry = self._lazy_registry()
app = serve_registry(registry, run=False)
assert not registry["group"].is_loaded("model")

client = starlette_testclient.TestClient(app)
response = client.post("/materialize", data={"path": "group/model"}, follow_redirects=False)

assert response.status_code == 303
assert "sel=group/model" in response.headers["location"]
assert registry["group"].is_loaded("model")
to_thread.assert_awaited_once()

def test_materialize_missing_path_redirects_without_error(self):
starlette_testclient = pytest.importorskip("starlette.testclient")
registry = self._lazy_registry()
app = serve_registry(registry, run=False)

client = starlette_testclient.TestClient(app)
response = client.post("/materialize", follow_redirects=False)

assert response.status_code == 303

def test_materialize_rejects_get(self):
starlette_testclient = pytest.importorskip("starlette.testclient")
app = serve_registry(self._lazy_registry(), run=False)

response = starlette_testclient.TestClient(app).get("/materialize", params={"path": "group/model"})

assert response.status_code == 405

def test_homepage_seeds_selected_model(self):
starlette_testclient = pytest.importorskip("starlette.testclient")
registry = self._lazy_registry()
app = serve_registry(registry, run=False)

client = starlette_testclient.TestClient(app)
assert "group/model" in client.get("/", params={"sel": "group/model"}).text
assert "group/model" not in client.get("/").text


class TestAssetLayout:
def test_selected_layout_has_runtime_asset(self):
# Guards the 404 regression: an unrelated top-level ``js`` package must not push us to the
# "source" layout, whose bundle directory would then lack spaday's runtime asset.
layout = _asset_layout()
runtime = _ASSETS[layout]["runtime"].lstrip("/")
assert (Path(bundles_dir(layout)) / runtime).is_file()
Loading