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
2 changes: 1 addition & 1 deletion examples/load_tsqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pathlib import Path
import sys

from timenet.client import TimeNet
from timenet import TimeNet


def main() -> None:
Expand Down
6 changes: 3 additions & 3 deletions examples/test_mean_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

from timenet.client import TimeNet
import timenet_connectors
import timenet
from timenet import TimeNet


# Build and load through the same explicit local registry.
registry = Path(".timenet-registry")
timenet_connectors.build("timenet/test-mean", out=registry)
timenet.build("timenet/test-mean", local_registry=registry)
dataset = TimeNet(sources=[registry]).load("timenet/test-mean")

# A quick tour of what we loaded: identity, counts, specs, and a record preview.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
(see :mod:`timenet_connectors.discovery`). Reusable bases, for example for the HuggingFace Hub, live
under ``timenet_connectors.bases``. The connector contract is :class:`timenet.connectors.BaseConnector`.

The module-level helpers :func:`build` and :func:`load` (from :mod:`timenet_connectors.api`) are the
producer-side shortcuts. Use them to build and consume a dataset from local code.
To build a dataset from Python, call ``timenet.build("org/name")``, and read it with
``TimeNet().load("org/name")``.
"""

from timenet_connectors.api import build as build, load as load
120 changes: 0 additions & 120 deletions packages/timenet-connectors/src/timenet_connectors/api.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,13 @@ def _build_to( # ruff: ignore[too-many-arguments]
) -> str:
"""Build a release for a local or remote output.

Remote outputs receive a copy of a build in the local registry. If the output already holds the release and
``force`` is not set, nothing runs and a status line says so.
Remote registries receive a copy from the local registry.
The build skips an existing release unless ``force`` is set.

Args:
dataset_id: The dataset ID.
target: The local output directory or remote registry.
fetch_from: The registry to copy missing parents from, or ``None`` to use the remote output if present.
fetch_from: The registry for missing parents, or ``None`` to use the remote output if present.
build_parents: Build missing parents with installed connectors.
force: Rebuild an existing local release.
options: Options for each build.
Expand Down
72 changes: 23 additions & 49 deletions packages/timenet-connectors/tests/test_build_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from timenet.types.reference_keys import ParentAlias, SourceKey
from timenet.types.references import DatasetRef
from timenet.values_backends import ValuesBackend
import timenet_connectors
from timenet_connectors.builder import backend as backend_module, cli as cli_module, env as env_module
from timenet_connectors.builder.backend import ConnectorBuilder
from timenet_connectors.builder.cli import app
Expand Down Expand Up @@ -535,45 +534,29 @@ def test_build_prepares_parents_before_the_isolated_child(tmp_path, monkeypatch)
assert result.stdout.strip() == str(out / CHILD.dataset_id / str(CHILD.version))


@pytest.mark.parametrize("obtain", ["fetch_from", "build_parents"])
def test_load_obtains_a_missing_parent_before_it_builds_and_reads(tmp_path, monkeypatch, obtain):
def test_every_build_entry_point_prepares_parents(tmp_path, monkeypatch, child_builds):
cards = _install_cards(monkeypatch, child={"parent": PARENT}, parent={})
monkeypatch.setenv("TIMENET_LOCAL_REGISTRY", str(tmp_path / "out"))
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _prepared_builds(calls))
origin = LocalRegistry(tmp_path / "origin")
parent = make_dataset()
parent._metadata = cards[PARENT.dataset_id]
origin.put_dataset(parent)

with pytest.raises(TimeNetMissingParentError, match=r"^parent release test/parent@1\.0\.0 of test/child") as caught:
timenet_connectors.load(CHILD.dataset_id)
assert str(caught.value).endswith(". Copy it with fetch_from=<registry> or build it with build_parents=True.")
if obtain == "fetch_from":
dataset = timenet_connectors.load(CHILD.dataset_id, fetch_from=origin)
else:
dataset = timenet_connectors.load(CHILD.dataset_id, build_parents=True)
out = tmp_path / "out"

assert dataset.metadata.dataset_id == CHILD.dataset_id
# The child holds the Record it imported from the parent.
assert [record.id for record in dataset.records] == [make_dataset().records[0].id]
builds = [(CHILD.dataset_id, False)]
assert calls == (builds if obtain == "fetch_from" else [(PARENT.dataset_id, False), *builds])
def build(origin=None):
# The client and timenet.build() build through the coordinator.
store = LocalRegistry(out)
run_plan(
plan_release_build(CHILD.dataset_id, store=store, parents_from=[] if origin is None else [origin]), store
)

with pytest.raises(TimeNetMissingParentError, match=r"parent release test/parent@1\.0\.0 of test/child@1\.0\.0"):
build()
assert child_builds == []

def test_load_copies_a_missing_payload_source_through_fetch_from(tmp_path, monkeypatch):
_install_cards(monkeypatch, parent={"parent": GRAND})
out = LocalRegistry(tmp_path / "out")
_parent_without_its_source(out)
# A registry instance to fetch from, as the SDK layers accept one.
origin = LocalRegistry(tmp_path / "origin")
origin.put_dataset(make_dataset())
monkeypatch.setenv("TIMENET_LOCAL_REGISTRY", str(out.root))

dataset = timenet_connectors.load(PARENT.dataset_id, fetch_from=origin)

assert len(dataset.records) == 2
# The build copies the payload source into the store, where the read finds it.
assert out.exists(GRAND.dataset_id, str(GRAND.version))
parent = make_dataset()
parent._metadata = cards[PARENT.dataset_id]
origin.put_dataset(parent)
build(origin)
assert child_builds == [(CHILD.dataset_id, False)]
assert LocalRegistry(out).exists(PARENT.dataset_id, str(PARENT.version))


def test_a_pinned_parent_build_hands_its_pins_to_the_isolated_child(tmp_path, monkeypatch):
Expand Down Expand Up @@ -813,26 +796,17 @@ def test_the_requested_build_waits_for_a_payload_source_of_a_held_parent(tmp_pat
)


@pytest.mark.parametrize(
("entry", "hint"), [("cli", "Build it with --build-parents."), ("api", "Copy it with fetch_from=<registry>.")]
)
def test_the_hint_names_only_the_option_that_the_build_did_not_use(tmp_path, monkeypatch, entry, hint):
# No registry holds the parent, and no installed connector builds it. The CLI build uses
# --fetch-from, and the API build uses build_parents=True.
def test_the_hint_names_only_the_option_that_the_build_did_not_use(tmp_path, monkeypatch):
# No registry holds the parent, and no installed connector builds it. The build uses --fetch-from.
cards = _install_cards(monkeypatch, child={"parent": PARENT})
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
out = tmp_path / "out"

if entry == "cli":
options = ["--fetch-from", str(tmp_path / "origin")]
error = runner.invoke(app, ["build", CHILD.dataset_id, "--out", str(out), *options]).exception
else:
with pytest.raises(TimeNetMissingParentError) as caught:
timenet_connectors.build(CHILD.dataset_id, out=out, build_parents=True)
error = caught.value
options = ["--fetch-from", str(tmp_path / "origin")]
error = runner.invoke(app, ["build", CHILD.dataset_id, "--out", str(out), *options]).exception

assert isinstance(error, TimeNetMissingParentError)
assert error.hint == hint
assert error.hint == "Build it with --build-parents."


def _count_reads(monkeypatch, registry) -> dict[str, int]:
Expand Down
25 changes: 1 addition & 24 deletions packages/timenet-connectors/tests/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@

from timenet.client import TimeNet
from timenet.config import settings
from timenet.errors import TimeFValidationError, TimeNetRegistryError
from timenet.errors import TimeNetRegistryError
from timenet.provenance import build_env
from timenet.registry import LocalRegistry, RemoteRegistry
from timenet.sources import InstalledConnectors
from timenet.testing import assert_datasets_equal
from timenet_connectors import build, load
from timenet_connectors.builder.cli import _resolve_target, app as build_app
from timenet_connectors.datasets.timenet.hello_world import HelloWorldConnector
from timenet_connectors.discovery import available, resolve
Expand Down Expand Up @@ -63,17 +62,6 @@ def test_build_unknown_id_fails(tmp_path):
assert result.exit_code != 0


def test_build_rejects_version_mismatch():
# The guard fires before the pipeline runs, so no registry is written.
with pytest.raises(TimeFValidationError, match="builds version"):
build("timenet/hello-world", version="9.9.9")


def test_load_rejects_version_mismatch():
with pytest.raises(TimeFValidationError, match="builds version"):
load("timenet/hello-world", "9.9.9")


# ---- where a bare `build` writes -----------------------------------------------------------------


Expand Down Expand Up @@ -111,17 +99,6 @@ def test_resolve_target_rejects_an_empty_out_instead_of_the_current_directory():
_resolve_target("")


def test_sdk_build_cleans_cache_but_keep_cache_retains(tmp_path, monkeypatch):
monkeypatch.setenv("TIMENET_ISOLATION", "off") # the in-process path, the one that kept the cache
cache = settings().downloads_dir / "timenet" / "hello-world"

build("timenet/hello-world", out=tmp_path / "r1")
assert not cache.exists()

build("timenet/hello-world", out=tmp_path / "r2", keep_cache=True)
assert cache.exists()


def test_build_cleans_cache_but_keep_flag_retains(tmp_path):
cache = settings().downloads_dir / "timenet" / "hello-world"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import pytest

import timenet
from timenet.client import TimeNet
from timenet.errors import TimeNetVersionUnavailableError
from timenet.registry import LocalRegistry
from timenet.testing import make_dataset
from timenet.types import ClassificationTask
from timenet.types.references import DatasetRef
import timenet_connectors
from timenet_connectors.builder import backend as backend_module


HELLO = DatasetRef.parse("timenet/hello-world@1.0.0")


def _unexpected_isolation(*args, **kwargs):
raise AssertionError(f"run_isolated_build should not have run: {args} {kwargs}")

Expand All @@ -17,16 +21,15 @@ def _unexpected_resolve(*args, **kwargs):
raise AssertionError(f"resolve should not have run: {args} {kwargs}")


def test_build_then_load_round_trip(monkeypatch):
# timenet_connectors.build/load anchor on the store of the default list, so a build here loads back.
def test_build_then_load_round_trip(monkeypatch, tmp_path):
# In-process, so the round trip stays in this interpreter instead of shelling out to uv.
monkeypatch.setenv("TIMENET_ISOLATION", "off")
monkeypatch.setattr(backend_module, "run_isolated_build", _unexpected_isolation)
out = tmp_path / "out"

version_dir = timenet_connectors.build("timenet/test-mean")
assert (version_dir / "manifest.json").exists()
assert timenet.build("timenet/test-mean", local_registry=out) == out / "timenet/test-mean/1.0.0"

dataset = timenet_connectors.load("timenet/test-mean")
dataset = TimeNet(sources=[str(out)]).load("timenet/test-mean")
assert len(dataset.records) == 1000

x, y = dataset.to_features_and_targets(task=ClassificationTask)
Expand All @@ -46,11 +49,10 @@ def fake_isolated(release, store_root, *, force=False, keep_cache=False, quiet=F

monkeypatch.setattr(backend_module, "run_isolated_build", fake_isolated)

version_dir = timenet_connectors.build("timenet/hello-world", out=str(tmp_path), force=True)
timenet.build("timenet/hello-world", local_registry=tmp_path / "out", force=True)

# The child is not quiet: it shows its downloads and shards.
assert calls == [(DatasetRef.parse("timenet/hello-world@1.0.0"), tmp_path, True, False, False)]
assert version_dir == tmp_path / "timenet" / "hello-world" / "1.0.0"
assert calls == [(HELLO, tmp_path / "out", True, False, False)]


def test_build_rejects_a_version_the_connector_does_not_declare(monkeypatch, tmp_path):
Expand All @@ -60,7 +62,7 @@ def test_build_rejects_a_version_the_connector_does_not_declare(monkeypatch, tmp
with pytest.raises(
TimeNetVersionUnavailableError, match=r"builds version 1\.0\.0, not the requested 9\.9\.9"
) as caught:
timenet_connectors.build("timenet/hello-world", version="9.9.9", out=tmp_path)
timenet.build("timenet/hello-world@9.9.9", local_registry=tmp_path / "out")

assert str(caught.value.declared) == "1.0.0"
assert not tmp_path.joinpath("timenet").exists()
assert not (tmp_path / "out").exists()
Loading
Loading