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
11 changes: 7 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Give each test an empty TimeNet home and no other ``TIMENET_*`` setting."""
"""Give each test an empty TimeNet store, registry and cache."""

import os

Expand All @@ -9,11 +9,14 @@

@pytest.fixture(autouse=True)
def _timenet_state(tmp_path_factory, monkeypatch):
"""Set an empty temporary TimeNet home and clear other ``TIMENET_*`` variables.
"""Set up an empty store, registry and cache for each test.

Ignore settings from ``.env`` files.
Clear existing ``TIMENET_*`` variables and ignore the ``.env`` file.
"""
for name in [name for name in os.environ if name.startswith("TIMENET_")]:
monkeypatch.delenv(name)
monkeypatch.setitem(TimeNetSettings.model_config, "env_file", None)
monkeypatch.setenv("TIMENET_HOME", str(tmp_path_factory.mktemp("timenet-home")))
root = tmp_path_factory.mktemp("timenet-state")
monkeypatch.setenv("TIMENET_LOCAL_REGISTRY", str(root / "store"))
monkeypatch.setenv("TIMENET_REMOTE_REGISTRY", str(root / "remote"))
monkeypatch.setenv("TIMENET_CACHE", str(root / "cache"))
38 changes: 16 additions & 22 deletions packages/timenet-connectors/src/timenet_connectors/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
"""Producer-side shortcuts to build and load a dataset from local code.

:func:`build` runs a connector through the engine into the shared local registry. :func:`load` runs
build and reads the result back. The writer and reader stacks are heavy, so they load lazily. This
keeps the import of :mod:`timenet_connectors` cheap.
"""
"""Build and load datasets with installed connectors."""

from pathlib import Path
from typing import TYPE_CHECKING
Expand All @@ -27,39 +22,38 @@ def build( # ruff: ignore[too-many-arguments]
# ruff: ignore[docstring-extraneous-exception] (raised by the plan and its run, not directly here)
"""Build a dataset release in a local registry.

The build reuses an existing release unless ``force`` is set. The connector runs in an environment
from its ``requirements.txt``, or in this interpreter if ``TIMENET_ISOLATION=off``.
The connector runs in an environment built from its ``requirements.txt``.
Set ``TIMENET_ISOLATION=off`` to use the current interpreter.

Args:
dataset_id: The ``org/name`` ID declared in the connector's card.
version: The required card version, or ``None`` to accept any version.
out: The local registry directory, or ``None`` to use the default local registry.
force: Rebuild an existing release.
keep_cache: Keep the raw download cache after a successful build.
fetch_from: The registry or URI to copy missing parents from before trying builds.
build_parents: Build missing parents with installed connectors.
out: The output directory, or ``None`` to use ``TIMENET_LOCAL_REGISTRY``.
force: Rebuild an existing release instead of reusing it.
keep_cache: Keep raw downloads after a successful build.
fetch_from: A registry or registry URI to copy missing parent releases from.
build_parents: Build missing parent releases from their installed connectors.

Returns:
The version directory of the release.
The release's version directory.

Raises:
TimeNetDatasetNotFoundError: If no installed connector builds ``dataset_id``.
TimeNetVersionUnavailableError: If the card's version differs from ``version``.
TimeNetMissingParentError: If a required release cannot be copied or built.
TimeNetVersionUnavailableError: If the card version differs from ``version``.
TimeNetMissingParentError: If a required parent release is missing.
"""
from timenet.builders import BuildOptions, declared # ruff: ignore[import-outside-top-level]
from timenet.config import settings # ruff: ignore[import-outside-top-level]
from timenet.coordinator.build import plan_release_build, run_plan # ruff: ignore[import-outside-top-level]
from timenet.errors import TimeNetMissingParentError # ruff: ignore[import-outside-top-level]
from timenet.registry import ( # ruff: ignore[import-outside-top-level]
LocalRegistry,
default_registry_path,
open_registry,
)
from timenet.registry import LocalRegistry, open_registry # ruff: ignore[import-outside-top-level]
from timenet.registry._paths import release_dir # ruff: ignore[import-outside-top-level, import-private-name]
from timenet.sources import InstalledConnectors # ruff: ignore[import-outside-top-level]

release = declared(dataset_id).release
root = Path(out).expanduser() if out is not None else default_registry_path()
# TODO(adrian): Remove temporary glue which maps a missing out onto the local registry of the default
# list until timenet.build() replaces this function.
root = Path(out).expanduser() if out is not None else settings().local_registry_dir
source = open_registry(fetch_from) if isinstance(fetch_from, str) else fetch_from
store = LocalRegistry(root)
try:
Expand Down
43 changes: 24 additions & 19 deletions packages/timenet-connectors/src/timenet_connectors/builder/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,25 @@ def _root(quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress stat


def _resolve_target(out: str | None) -> WritableRegistry | Path:
"""Resolve the registry or local directory for build output.

It tries ``out``, then ``$TIMENET_REGISTRY``, then the default local registry.
"""Resolve a build output to a local directory or writable remote registry.

Args:
out: The output URL or path, or ``None`` to use the configured default.
out: The output URL or path, or ``None`` to use ``TIMENET_LOCAL_REGISTRY``.

Returns:
A registry for a remote URL, or a ``Path`` for a local directory.
"""
target = out if out is not None else settings().registry
if target is None:
return settings().registry_path
A local directory ``Path`` or a remote ``WritableRegistry``.

Raises:
TimeNetRegistryError: If ``out`` is blank or cannot be opened as a registry.
""" # ruff: ignore[docstring-extraneous-exception] - raised by open_registry
# TODO(adrian): Remove temporary glue which maps a missing --out onto the local registry of the
Comment thread
karhu marked this conversation as resolved.
Resolved
# default list until the build CLI takes the flags of the defaults.
if out is None:
return settings().local_registry_dir
try:
return local_registry_path(target)
return local_registry_path(out, label="--out")
except TimeNetRegistryError:
return open_registry(target)
return open_registry(out, label="--out")


@app.command()
Expand All @@ -75,7 +77,7 @@ def build(
None,
"--out",
help="Output registry: a local directory or a timenet:// / http(s):// / s3:// URL "
"(default: $TIMENET_REGISTRY, else the local registry).",
"(default: the local registry, $TIMENET_LOCAL_REGISTRY).",
),
force: bool = typer.Option(
False, "--force", "-f", help="Build the release again, even if the local registry holds it."
Expand Down Expand Up @@ -202,20 +204,21 @@ def _build_to( # ruff: ignore[too-many-arguments]
force: bool,
options: BuildOptions,
) -> str:
"""Build a release locally and copy it to a remote output if needed.
"""Build a release for a local or remote output.

Reuse an existing release unless ``force`` is true.
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.

Args:
dataset_id: The dataset ID.
target: The local directory or remote registry to write to.
fetch_from: The registry to copy missing parents from. ``None`` uses a remote output, if any.
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.
build_parents: Build missing parents with installed connectors.
force: Rebuild an existing local release.
options: Settings for each build.
options: Options for each build.

Returns:
The local version directory or the remote version string.
The local version directory or remote version string.

Raises:
BadParameter: If ``force`` is set and the remote output already holds the version.
Expand All @@ -233,7 +236,9 @@ def _build_to( # ruff: ignore[too-many-arguments]
console.success(f"Nothing to do: '{release.dataset_id}' {release.version} is already in the remote --out")
return str(release.version)
else:
store, remote = LocalRegistry(settings().registry_path), target
# TODO(adrian): Remove temporary glue which maps a remote --out onto a build into the
# local registry of the default list until --publish-to replaces a remote --out.
store, remote = LocalRegistry(settings().local_registry_dir), target
# A build into the remote registry found the parents there. So without --fetch-from, the build
# copies a missing parent from the remote registry into the local one.
fetch_from = fetch_from if fetch_from is not None else remote
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def test_pipeline_preserves_source_semantics_after_source_removal(monkeypatch, t
# Ten rows cover multiline features with/without location and a partial task batch.
monkeypatch.setattr(control_writer, "_TASK_BATCH_SIZE", 3)
monkeypatch.setenv("TIMENET_CACHE", str(tmp_path / "cache"))
cache = settings().cache_dir / "bash-lab/sensorcaps"
cache = settings().downloads_dir / "bash-lab/sensorcaps"

async def download_async(self, cache_dir):
assert cache_dir == cache
Expand Down
1 change: 0 additions & 1 deletion packages/timenet-connectors/tests/test_build_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ def test_build_in_process_round_trips_the_connector_default_zarr_backend(tmp_pat
connector_cls = backend_module.resolve("timenet/hello-world")
monkeypatch.setattr(connector_cls, "values_backend", ValuesBackend.ZARR)
monkeypatch.setenv("TIMENET_ISOLATION", "off")
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
store = LocalRegistry(tmp_path / "registry")

ConnectorBuilder().build(HELLO, store, force=False, pinned_by={}, options=BuildOptions())
Expand Down
7 changes: 2 additions & 5 deletions packages/timenet-connectors/tests/test_build_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,9 @@ def test_build_isolates_when_the_flag_overrides_the_setting(monkeypatch, tmp_pat
assert len(calls) == 1


def test_build_without_isolation_shows_each_build_line_once(monkeypatch, tmp_path):
def test_build_without_isolation_shows_each_build_line_once(tmp_path):
# In this process, the coordinator sends the build step, and the renderer shows it. Nothing else
# prints a build line.
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))

result = runner.invoke(app, ["build", "timenet/hello-world", "--out", str(tmp_path / "out"), "--no-isolation"])

Expand Down Expand Up @@ -177,7 +176,7 @@ def test_isolated_build_prints_the_version_dir_and_narrates_once(tmp_path):
# the child's inherited stderr is captured here too, where a double-narration regression would
# surface: the parent and the child both see the build step.
registry = tmp_path / "registry"
env = {**os.environ, "TIMENET_HOME": str(tmp_path / "home")}
env = dict(os.environ) # the store, the remote registry and the cache of this test
env.pop("TIMENET_ISOLATION", None) # default is "on"; make sure nothing forces it off
run_cli = [sys.executable, "-c", "from timenet_connectors.builder.cli import main; main()"]
result = subprocess.run(
Expand Down Expand Up @@ -246,7 +245,6 @@ def test_the_build_prepared_release_command_does_not_plan(monkeypatch, tmp_path)
monkeypatch.setattr(ConnectorBuilder, "card", lambda self, dataset_id: connectors[dataset_id]().metadata())
monkeypatch.setattr(backend_module, "resolve", connectors.__getitem__)
monkeypatch.setenv("TIMENET_ISOLATION", "off")
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setattr(cli_module.console, "quiet", False)

# The store lacks the parent. A planner would build it first. The command must fail on the
Expand All @@ -261,7 +259,6 @@ def test_the_build_prepared_release_command_does_not_plan(monkeypatch, tmp_path)

def test_the_build_prepared_release_command_builds_a_release_without_parents(monkeypatch, tmp_path):
monkeypatch.setenv("TIMENET_ISOLATION", "off")
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setattr(cli_module.console, "quiet", False)

result = runner.invoke(
Expand Down
14 changes: 5 additions & 9 deletions packages/timenet-connectors/tests/test_build_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,7 @@ def test_build_prepares_parents_before_the_isolated_child(tmp_path, monkeypatch)
@pytest.mark.parametrize("obtain", ["fetch_from", "build_parents"])
def test_load_obtains_a_missing_parent_before_it_builds_and_reads(tmp_path, monkeypatch, obtain):
cards = _install_cards(monkeypatch, child={"parent": PARENT}, parent={})
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setenv("TIMENET_REGISTRY", str(tmp_path / "out"))
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")
Expand Down Expand Up @@ -568,8 +567,7 @@ def test_load_copies_a_missing_payload_source_through_fetch_from(tmp_path, monke
_parent_without_its_source(out)
origin = LocalRegistry(tmp_path / "origin")
origin.put_dataset(make_dataset())
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setenv("TIMENET_REGISTRY", str(out.root))
monkeypatch.setenv("TIMENET_LOCAL_REGISTRY", str(out.root))

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

Expand Down Expand Up @@ -616,7 +614,6 @@ def child(command, env):
def test_the_cli_hint_names_its_flags(tmp_path, monkeypatch, case):
cards = _install_cards(monkeypatch, child={"parent": PARENT}, parent={})
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
out = LocalRegistry(tmp_path / "out")
if case == "damaged":
out.put_dataset(TimeFDataset(metadata=cards[PARENT.dataset_id]))
Expand Down Expand Up @@ -655,15 +652,15 @@ def _remote_out(tmp_path, monkeypatch, cards) -> tuple[_Hosted, LocalRegistry, l
"""Make the ``--out`` of the CLI a registry that is not local, and record each isolated build.

Returns:
The registry that ``--out`` names, the default local registry, and the list of the builds.
The registry that ``--out`` names, the store, and the list of the builds.
"""
remote = _Hosted(tmp_path / "hosted")
monkeypatch.setattr(cli_module, "_resolve_target", lambda target: remote)
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
monkeypatch.setenv("TIMENET_LOCAL_REGISTRY", str(tmp_path / "store"))
calls = []
monkeypatch.setattr(backend_module, "run_isolated_build", _prepared_builds(calls))
return remote, LocalRegistry(tmp_path / "home/registry"), calls
return remote, LocalRegistry(tmp_path / "store"), calls


def test_build_to_a_remote_out_refuses_a_credentialed_release_before_any_download(tmp_path, monkeypatch):
Expand Down Expand Up @@ -824,7 +821,6 @@ def test_the_hint_names_only_the_option_that_the_build_did_not_use(tmp_path, mon
# --fetch-from, and the API build uses build_parents=True.
cards = _install_cards(monkeypatch, child={"parent": PARENT})
monkeypatch.setattr(cli_module, "known_ids", lambda: list(cards))
monkeypatch.setenv("TIMENET_HOME", str(tmp_path / "home"))
out = tmp_path / "out"

if entry == "cli":
Expand Down
Loading
Loading