Skip to content

Commit 0959042

Browse files
committed
Support fsspec specifiers for ImageBuilder envs
Addresses #42
1 parent a8e6761 commit 0959042

3 files changed

Lines changed: 37 additions & 18 deletions

File tree

docs/xcetool.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ The command-line interface to xcengine is the command `xcetool`, which
66
implements multiple subcommands and options for building and running
77
container images and Application Packages.
88

9-
You can use the `--help` flag for any `xcetool` command or subcommand to get more
10-
details on usage and available options.
9+
You can use the `--help` flag for any `xcetool` command or subcommand to get
10+
more details on usage and available options.
1111

1212
### `xcetool image build`
1313

@@ -16,9 +16,9 @@ Usage: `xcetool image build [OPTIONS] NOTEBOOK`
1616
This is the main `xcetool` subcommand: it builds a container image from a
1717
supplied notebook and environment file. If given the `--eoap` argument, it also
1818
generates a CWL file defining a corresponding application package. The
19-
NOTEBOOK argument can be a path to a local file, a URL, or any other string
20-
which can be parsed by the [fsspec](https://filesystem-spec.readthedocs.io/)
21-
library.
19+
NOTEBOOK argument can be a path to a local file, an HTTP URL, or any other
20+
string which can be parsed by the
21+
[fsspec](https://filesystem-spec.readthedocs.io/) library.
2222

2323
Options:
2424

@@ -28,6 +28,8 @@ Options:
2828
This option is mainly useful for debugging.
2929
- `-e`, `--environment` `FILE`:
3030
Conda environment file to use in Docker image.
31+
This can be a path to a local file, an HTTP URL, or any other string which
32+
can be parsed by the fsspec library.
3133
If no environment file is specified here or in the notebook,
3234
xcetool will look for a file called `environment.yml`
3335
in the notebook's directory. If all else fails,

test/test_core.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,24 +380,41 @@ def test_image_builder_write_dockerfile(tmp_path):
380380

381381

382382
@patch("docker.from_env")
383-
@pytest.mark.parametrize("set_env", [False, True])
383+
@pytest.mark.parametrize("env_type", ["none", "local", "http"])
384384
@pytest.mark.parametrize("skip_build", [False, True])
385-
def test_image_builder_build_dir(from_env_mock, tmp_path, set_env, skip_build):
385+
def test_image_builder_build_dir(
386+
from_env_mock,
387+
tmp_path,
388+
httpserver,
389+
env_type,
390+
skip_build
391+
):
386392
client_mock = Mock(docker.client.DockerClient)
387393
client_mock.images.build.return_value = None, None
388394
from_env_mock.return_value = client_mock
389395

390396
build_dir = tmp_path / "build"
391-
env_path = tmp_path / "env2.yaml"
397+
build_env_path = tmp_path / "env2.yaml"
392398
env_def = {
393399
"name": "foo",
394400
"channels": "bar",
395401
"dependencies": ["python >=3.13", "baz >=42.0"],
396402
}
397-
env_path.write_text(yaml.safe_dump(env_def))
403+
build_env_path.write_text(yaml.safe_dump(env_def))
404+
env_http = "/env2.yaml"
405+
406+
match env_type:
407+
case "none": env_param = None
408+
case "local": env_param = build_env_path
409+
case "http":
410+
httpserver.expect_request(env_http).respond_with_data(build_env_path.read_bytes())
411+
env_param = httpserver.url_for(env_http)
412+
case _:
413+
raise RuntimeError(f"Unknown env type {env_type}")
414+
398415
image_builder = ImageBuilder(
399416
pathlib.Path(__file__).parent / "data" / "noparamtest.ipynb",
400-
env_path if set_env else None,
417+
env_param,
401418
build_dir,
402419
None,
403420
)
@@ -406,11 +423,11 @@ def test_image_builder_build_dir(from_env_mock, tmp_path, set_env, skip_build):
406423
from_env_mock.assert_not_called()
407424
else:
408425
client_mock.images.build.assert_called()
409-
env_path = build_dir / "environment.yml"
410-
assert env_path.is_file()
411-
output_env = yaml.safe_load(env_path.read_text())
426+
build_env_path = build_dir / "environment.yml"
427+
assert build_env_path.is_file()
428+
output_env = yaml.safe_load(build_env_path.read_text())
412429
assert {"name", "channels", "dependencies"} <= set(output_env)
413-
if set_env:
430+
if env_type != "none":
414431
assert output_env["name"] == env_def["name"]
415432
assert output_env["channels"] == env_def["channels"]
416433
assert set(output_env["dependencies"]) >= set(env_def["dependencies"])

xcengine/core.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,12 @@ class ImageBuilder:
187187
"""
188188

189189
tag_format: ClassVar[str] = "%Y.%m.%d.%H.%M.%S"
190-
environment: pathlib.Path | None = None
190+
environment: pathlib.Path | str | None = None
191191

192192
def __init__(
193193
self,
194-
notebook: pathlib.Path,
195-
environment: pathlib.Path | None,
194+
notebook: pathlib.Path | str,
195+
environment: pathlib.Path | str | None,
196196
build_dir: pathlib.Path,
197197
tag: str | None,
198198
):
@@ -244,7 +244,7 @@ def build(
244244
) -> Image | None:
245245
self.script_creator.convert_notebook_to_script(self.build_dir)
246246
if self.environment:
247-
with open(self.environment, "r") as fh:
247+
with fsspec.open(self.environment, "r") as fh:
248248
env_def = yaml.safe_load(fh)
249249
else:
250250
LOGGER.warning(

0 commit comments

Comments
 (0)