lyceum-cli 1.0.25__py3-none-any.whl → 1.0.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lyceum/external/auth/login.py +18 -18
- lyceum/external/compute/execution/docker.py +4 -2
- lyceum/external/compute/execution/docker_compose.py +263 -0
- lyceum/external/compute/execution/notebook.py +0 -2
- lyceum/external/compute/execution/python.py +2 -1
- lyceum/external/compute/inference/batch.py +8 -10
- lyceum/external/vms/instances.py +301 -0
- lyceum/external/vms/management.py +383 -0
- lyceum/main.py +3 -0
- lyceum/shared/config.py +19 -24
- lyceum/shared/display.py +12 -31
- lyceum/shared/streaming.py +17 -45
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/METADATA +1 -1
- lyceum_cli-1.0.27.dist-info/RECORD +34 -0
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/WHEEL +1 -1
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/top_level.txt +0 -1
- lyceum/external/compute/execution/docker_config.py +0 -123
- lyceum/external/storage/files.py +0 -273
- lyceum_cli-1.0.25.dist-info/RECORD +0 -46
- tests/__init__.py +0 -1
- tests/conftest.py +0 -200
- tests/unit/__init__.py +0 -1
- tests/unit/external/__init__.py +0 -1
- tests/unit/external/compute/__init__.py +0 -1
- tests/unit/external/compute/execution/__init__.py +0 -1
- tests/unit/external/compute/execution/test_data.py +0 -33
- tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
- tests/unit/external/compute/execution/test_python_helpers.py +0 -406
- tests/unit/external/compute/execution/test_python_run.py +0 -289
- tests/unit/shared/__init__.py +0 -1
- tests/unit/shared/test_config.py +0 -341
- tests/unit/shared/test_streaming.py +0 -259
- /lyceum/external/{storage → vms}/__init__.py +0 -0
- {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/entry_points.txt +0 -0
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
"""Tests for DependencyResolver class."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from lyceum.external.compute.execution.python import DependencyResolver
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestDependencyResolverIsStdlib:
|
|
13
|
-
"""Tests for DependencyResolver.is_stdlib() method."""
|
|
14
|
-
|
|
15
|
-
def test_returns_false_for_local_path(self, sample_workspace):
|
|
16
|
-
resolver = DependencyResolver(sample_workspace)
|
|
17
|
-
local_file = sample_workspace / "mypackage" / "utils.py"
|
|
18
|
-
|
|
19
|
-
assert resolver.is_stdlib(local_file) is False
|
|
20
|
-
|
|
21
|
-
def test_returns_false_for_workspace_path(self, sample_workspace):
|
|
22
|
-
resolver = DependencyResolver(sample_workspace)
|
|
23
|
-
main_file = sample_workspace / "main.py"
|
|
24
|
-
|
|
25
|
-
assert resolver.is_stdlib(main_file) is False
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class TestDependencyResolverIsStdlibModule:
|
|
29
|
-
"""Tests for DependencyResolver.is_stdlib_module() method."""
|
|
30
|
-
|
|
31
|
-
def test_json_is_stdlib(self):
|
|
32
|
-
# json has a file origin in the stdlib
|
|
33
|
-
resolver = DependencyResolver(Path("/tmp"))
|
|
34
|
-
assert resolver.is_stdlib_module("json") is True
|
|
35
|
-
|
|
36
|
-
def test_pathlib_is_stdlib(self):
|
|
37
|
-
resolver = DependencyResolver(Path("/tmp"))
|
|
38
|
-
assert resolver.is_stdlib_module("pathlib") is True
|
|
39
|
-
|
|
40
|
-
def test_re_is_stdlib(self):
|
|
41
|
-
resolver = DependencyResolver(Path("/tmp"))
|
|
42
|
-
assert resolver.is_stdlib_module("re") is True
|
|
43
|
-
|
|
44
|
-
def test_collections_is_stdlib(self):
|
|
45
|
-
resolver = DependencyResolver(Path("/tmp"))
|
|
46
|
-
assert resolver.is_stdlib_module("collections") is True
|
|
47
|
-
|
|
48
|
-
def test_nonexistent_module(self):
|
|
49
|
-
resolver = DependencyResolver(Path("/tmp"))
|
|
50
|
-
assert resolver.is_stdlib_module("nonexistent_module_xyz_abc_123") is False
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class TestDependencyResolverFindImports:
|
|
54
|
-
"""Tests for DependencyResolver.find_imports() method."""
|
|
55
|
-
|
|
56
|
-
def test_finds_local_imports(self, sample_workspace):
|
|
57
|
-
sys.path.insert(0, str(sample_workspace))
|
|
58
|
-
try:
|
|
59
|
-
resolver = DependencyResolver(sample_workspace)
|
|
60
|
-
main_file = sample_workspace / "main.py"
|
|
61
|
-
resolver.find_imports(main_file, main_file)
|
|
62
|
-
|
|
63
|
-
# Should find mypackage and standalone
|
|
64
|
-
local_files = {p.name for p in resolver.local_imports}
|
|
65
|
-
assert "standalone.py" in local_files
|
|
66
|
-
# At minimum, we should find some local files
|
|
67
|
-
assert len(resolver.local_imports) > 0
|
|
68
|
-
finally:
|
|
69
|
-
sys.path.remove(str(sample_workspace))
|
|
70
|
-
|
|
71
|
-
def test_excludes_stdlib(self, sample_workspace):
|
|
72
|
-
sys.path.insert(0, str(sample_workspace))
|
|
73
|
-
try:
|
|
74
|
-
resolver = DependencyResolver(sample_workspace)
|
|
75
|
-
main_file = sample_workspace / "main.py"
|
|
76
|
-
resolver.find_imports(main_file, main_file)
|
|
77
|
-
|
|
78
|
-
# Should not include stdlib modules like os, sys, etc.
|
|
79
|
-
for path in resolver.local_imports:
|
|
80
|
-
# None of the local imports should be from site-packages
|
|
81
|
-
assert "site-packages" not in str(path)
|
|
82
|
-
finally:
|
|
83
|
-
sys.path.remove(str(sample_workspace))
|
|
84
|
-
|
|
85
|
-
def test_handles_relative_imports(self, sample_workspace):
|
|
86
|
-
sys.path.insert(0, str(sample_workspace))
|
|
87
|
-
try:
|
|
88
|
-
resolver = DependencyResolver(sample_workspace)
|
|
89
|
-
init_file = sample_workspace / "mypackage" / "__init__.py"
|
|
90
|
-
main_file = sample_workspace / "main.py"
|
|
91
|
-
|
|
92
|
-
resolver.find_imports(init_file, main_file)
|
|
93
|
-
|
|
94
|
-
# Should resolve relative import to utils
|
|
95
|
-
paths = [str(p) for p in resolver.local_imports]
|
|
96
|
-
assert any("utils.py" in p for p in paths)
|
|
97
|
-
finally:
|
|
98
|
-
sys.path.remove(str(sample_workspace))
|
|
99
|
-
|
|
100
|
-
def test_handles_nonexistent_file(self, sample_workspace):
|
|
101
|
-
resolver = DependencyResolver(sample_workspace)
|
|
102
|
-
nonexistent = sample_workspace / "nonexistent.py"
|
|
103
|
-
|
|
104
|
-
# Should not raise
|
|
105
|
-
resolver.find_imports(nonexistent, nonexistent)
|
|
106
|
-
assert len(resolver.local_imports) == 0
|
|
107
|
-
|
|
108
|
-
def test_handles_syntax_error_file(self, tmp_path):
|
|
109
|
-
bad_file = tmp_path / "bad_syntax.py"
|
|
110
|
-
bad_file.write_text("def broken(:\n pass")
|
|
111
|
-
|
|
112
|
-
resolver = DependencyResolver(tmp_path)
|
|
113
|
-
# Should not raise, just skip the file
|
|
114
|
-
resolver.find_imports(bad_file, bad_file)
|
|
115
|
-
|
|
116
|
-
def test_handles_directory(self, sample_workspace):
|
|
117
|
-
sys.path.insert(0, str(sample_workspace))
|
|
118
|
-
try:
|
|
119
|
-
resolver = DependencyResolver(sample_workspace)
|
|
120
|
-
pkg_dir = sample_workspace / "mypackage"
|
|
121
|
-
|
|
122
|
-
# Should follow directory to __init__.py
|
|
123
|
-
resolver.find_imports(pkg_dir, sample_workspace / "main.py")
|
|
124
|
-
|
|
125
|
-
# Should have processed the package
|
|
126
|
-
assert len(resolver.visited) > 0
|
|
127
|
-
finally:
|
|
128
|
-
sys.path.remove(str(sample_workspace))
|
|
129
|
-
|
|
130
|
-
def test_visited_set_prevents_infinite_recursion(self, sample_workspace):
|
|
131
|
-
sys.path.insert(0, str(sample_workspace))
|
|
132
|
-
try:
|
|
133
|
-
resolver = DependencyResolver(sample_workspace)
|
|
134
|
-
main_file = sample_workspace / "main.py"
|
|
135
|
-
|
|
136
|
-
# Call find_imports multiple times on same file
|
|
137
|
-
resolver.find_imports(main_file, main_file)
|
|
138
|
-
visited_count = len(resolver.visited)
|
|
139
|
-
|
|
140
|
-
resolver.find_imports(main_file, main_file)
|
|
141
|
-
# Should not add more to visited
|
|
142
|
-
assert len(resolver.visited) == visited_count
|
|
143
|
-
finally:
|
|
144
|
-
sys.path.remove(str(sample_workspace))
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class TestDependencyResolverCalculateImportPath:
|
|
148
|
-
"""Tests for DependencyResolver.calculate_import_path() method."""
|
|
149
|
-
|
|
150
|
-
def test_relative_import_path(self, sample_workspace):
|
|
151
|
-
resolver = DependencyResolver(sample_workspace)
|
|
152
|
-
actual_path = sample_workspace / "mypackage" / "utils.py"
|
|
153
|
-
main_file = sample_workspace / "main.py"
|
|
154
|
-
|
|
155
|
-
result = resolver.calculate_import_path(
|
|
156
|
-
actual_path.resolve(), main_file.resolve(), modname="utils", is_relative=True
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
assert result == "mypackage/utils.py"
|
|
160
|
-
|
|
161
|
-
def test_absolute_import_path_init(self, sample_workspace):
|
|
162
|
-
resolver = DependencyResolver(sample_workspace)
|
|
163
|
-
actual_path = sample_workspace / "mypackage" / "__init__.py"
|
|
164
|
-
main_file = sample_workspace / "main.py"
|
|
165
|
-
|
|
166
|
-
result = resolver.calculate_import_path(
|
|
167
|
-
actual_path.resolve(), main_file.resolve(), modname="mypackage", is_relative=False
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
assert result == "mypackage/__init__.py"
|
|
171
|
-
|
|
172
|
-
def test_absolute_import_path_module(self, sample_workspace):
|
|
173
|
-
resolver = DependencyResolver(sample_workspace)
|
|
174
|
-
actual_path = sample_workspace / "standalone.py"
|
|
175
|
-
main_file = sample_workspace / "main.py"
|
|
176
|
-
|
|
177
|
-
result = resolver.calculate_import_path(
|
|
178
|
-
actual_path.resolve(), main_file.resolve(), modname="standalone", is_relative=False
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
assert result == "standalone.py"
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
class TestDependencyResolverClassifyFile:
|
|
185
|
-
"""Tests for DependencyResolver.classify_file() method."""
|
|
186
|
-
|
|
187
|
-
def test_local_file_in_project(self, sample_workspace):
|
|
188
|
-
resolver = DependencyResolver(sample_workspace)
|
|
189
|
-
local_file = sample_workspace / "main.py"
|
|
190
|
-
|
|
191
|
-
assert resolver.classify_file(local_file) == "local"
|
|
192
|
-
|
|
193
|
-
def test_local_file_in_subpackage(self, sample_workspace):
|
|
194
|
-
resolver = DependencyResolver(sample_workspace)
|
|
195
|
-
pkg_file = sample_workspace / "mypackage" / "utils.py"
|
|
196
|
-
|
|
197
|
-
assert resolver.classify_file(pkg_file) == "local"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class TestDependencyResolverResolveRelativeImport:
|
|
201
|
-
"""Tests for DependencyResolver.resolve_relative_import() method."""
|
|
202
|
-
|
|
203
|
-
def test_resolves_relative_module(self, sample_workspace):
|
|
204
|
-
import ast
|
|
205
|
-
|
|
206
|
-
resolver = DependencyResolver(sample_workspace)
|
|
207
|
-
init_file = sample_workspace / "mypackage" / "__init__.py"
|
|
208
|
-
|
|
209
|
-
# Create a mock ImportFrom node for "from .utils import helper_function"
|
|
210
|
-
node = ast.ImportFrom(module="utils", names=[], level=1)
|
|
211
|
-
|
|
212
|
-
result = resolver.resolve_relative_import(init_file, node, "utils")
|
|
213
|
-
|
|
214
|
-
assert result is not None
|
|
215
|
-
assert "utils.py" in str(result)
|
|
216
|
-
|
|
217
|
-
def test_resolves_package_level_import(self, sample_workspace):
|
|
218
|
-
import ast
|
|
219
|
-
|
|
220
|
-
resolver = DependencyResolver(sample_workspace)
|
|
221
|
-
utils_file = sample_workspace / "mypackage" / "utils.py"
|
|
222
|
-
|
|
223
|
-
# Create a mock ImportFrom node for "from . import something" (level=1, no module)
|
|
224
|
-
node = ast.ImportFrom(module=None, names=[], level=1)
|
|
225
|
-
|
|
226
|
-
result = resolver.resolve_relative_import(utils_file, node, None)
|
|
227
|
-
|
|
228
|
-
# Should resolve to __init__.py of the package
|
|
229
|
-
if result:
|
|
230
|
-
assert "__init__.py" in str(result)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
class TestDependencyResolverParseFile:
|
|
234
|
-
"""Tests for DependencyResolver.parse_file() method."""
|
|
235
|
-
|
|
236
|
-
def test_parses_valid_file(self, sample_workspace):
|
|
237
|
-
resolver = DependencyResolver(sample_workspace)
|
|
238
|
-
main_file = sample_workspace / "main.py"
|
|
239
|
-
|
|
240
|
-
result = resolver.parse_file(main_file)
|
|
241
|
-
|
|
242
|
-
assert result is not None
|
|
243
|
-
|
|
244
|
-
def test_returns_none_for_syntax_error(self, tmp_path):
|
|
245
|
-
bad_file = tmp_path / "bad.py"
|
|
246
|
-
bad_file.write_text("def broken(:\n pass")
|
|
247
|
-
|
|
248
|
-
resolver = DependencyResolver(tmp_path)
|
|
249
|
-
result = resolver.parse_file(bad_file)
|
|
250
|
-
|
|
251
|
-
assert result is None
|
|
252
|
-
|
|
253
|
-
def test_returns_none_for_nonexistent(self, tmp_path):
|
|
254
|
-
resolver = DependencyResolver(tmp_path)
|
|
255
|
-
result = resolver.parse_file(tmp_path / "nonexistent.py")
|
|
256
|
-
|
|
257
|
-
assert result is None
|
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
"""Tests for Python execution helper functions."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from unittest.mock import MagicMock, patch
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
from click.exceptions import Exit as ClickExit
|
|
11
|
-
|
|
12
|
-
from lyceum.external.compute.execution.python import (
|
|
13
|
-
build_payload,
|
|
14
|
-
inject_script_args,
|
|
15
|
-
load_workspace_config,
|
|
16
|
-
read_code_from_source,
|
|
17
|
-
resolve_import_files,
|
|
18
|
-
resolve_requirements,
|
|
19
|
-
submit_execution,
|
|
20
|
-
validate_machine_type,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class TestReadCodeFromSource:
|
|
25
|
-
"""Tests for read_code_from_source function."""
|
|
26
|
-
|
|
27
|
-
def test_reads_from_file(self, tmp_path):
|
|
28
|
-
test_file = tmp_path / "test.py"
|
|
29
|
-
test_file.write_text("print('hello')")
|
|
30
|
-
|
|
31
|
-
code, file_path, file_name = read_code_from_source(str(test_file))
|
|
32
|
-
|
|
33
|
-
assert code == "print('hello')"
|
|
34
|
-
assert file_path == test_file
|
|
35
|
-
assert file_name == "test.py"
|
|
36
|
-
|
|
37
|
-
def test_returns_inline_code(self):
|
|
38
|
-
code, file_path, file_name = read_code_from_source("print('inline')")
|
|
39
|
-
|
|
40
|
-
assert code == "print('inline')"
|
|
41
|
-
assert file_path is None
|
|
42
|
-
assert file_name is None
|
|
43
|
-
|
|
44
|
-
def test_reads_multiline_file(self, tmp_path):
|
|
45
|
-
test_file = tmp_path / "multi.py"
|
|
46
|
-
test_file.write_text("line1\nline2\nline3")
|
|
47
|
-
|
|
48
|
-
code, file_path, file_name = read_code_from_source(str(test_file))
|
|
49
|
-
|
|
50
|
-
assert code == "line1\nline2\nline3"
|
|
51
|
-
assert file_name == "multi.py"
|
|
52
|
-
|
|
53
|
-
def test_with_status_line(self, tmp_path):
|
|
54
|
-
test_file = tmp_path / "test.py"
|
|
55
|
-
test_file.write_text("print('hello')")
|
|
56
|
-
|
|
57
|
-
mock_status = MagicMock()
|
|
58
|
-
code, file_path, file_name = read_code_from_source(str(test_file), status=mock_status)
|
|
59
|
-
|
|
60
|
-
mock_status.update.assert_called_once()
|
|
61
|
-
assert "test.py" in mock_status.update.call_args[0][0]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class TestInjectScriptArgs:
|
|
65
|
-
"""Tests for inject_script_args function."""
|
|
66
|
-
|
|
67
|
-
def test_injects_args(self):
|
|
68
|
-
code = "print(sys.argv)"
|
|
69
|
-
result = inject_script_args(code, ["--flag", "value"], "script.py")
|
|
70
|
-
|
|
71
|
-
assert "sys.argv" in result
|
|
72
|
-
assert "script.py" in result
|
|
73
|
-
assert "--flag" in result
|
|
74
|
-
assert "value" in result
|
|
75
|
-
|
|
76
|
-
def test_no_injection_without_args(self):
|
|
77
|
-
code = "print('hello')"
|
|
78
|
-
result = inject_script_args(code, [], "script.py")
|
|
79
|
-
|
|
80
|
-
assert result == code
|
|
81
|
-
|
|
82
|
-
def test_uses_default_script_name(self):
|
|
83
|
-
code = "print(sys.argv)"
|
|
84
|
-
result = inject_script_args(code, ["arg1"], None)
|
|
85
|
-
|
|
86
|
-
assert "script.py" in result
|
|
87
|
-
|
|
88
|
-
def test_preserves_original_code(self):
|
|
89
|
-
code = "original = 'code'\nprint(original)"
|
|
90
|
-
result = inject_script_args(code, ["--test"], "test.py")
|
|
91
|
-
|
|
92
|
-
assert "original = 'code'" in result
|
|
93
|
-
assert "print(original)" in result
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class TestResolveRequirements:
|
|
97
|
-
"""Tests for resolve_requirements function."""
|
|
98
|
-
|
|
99
|
-
def test_from_file(self, tmp_path):
|
|
100
|
-
req_file = tmp_path / "requirements.txt"
|
|
101
|
-
req_file.write_text("numpy==1.24.0\npandas>=2.0.0")
|
|
102
|
-
|
|
103
|
-
result = resolve_requirements(str(req_file), None)
|
|
104
|
-
|
|
105
|
-
assert "numpy==1.24.0" in result
|
|
106
|
-
assert "pandas>=2.0.0" in result
|
|
107
|
-
|
|
108
|
-
def test_from_string(self):
|
|
109
|
-
result = resolve_requirements("numpy>=1.0", None)
|
|
110
|
-
|
|
111
|
-
assert result == "numpy>=1.0"
|
|
112
|
-
|
|
113
|
-
def test_from_workspace_config(self):
|
|
114
|
-
workspace_config = {"dependencies": {"merged": ["numpy==1.24.0", "pandas>=2.0.0"]}}
|
|
115
|
-
|
|
116
|
-
result = resolve_requirements(None, workspace_config)
|
|
117
|
-
|
|
118
|
-
assert "numpy==1.24.0" in result
|
|
119
|
-
assert "pandas>=2.0.0" in result
|
|
120
|
-
|
|
121
|
-
def test_returns_none_when_empty(self):
|
|
122
|
-
result = resolve_requirements(None, None)
|
|
123
|
-
|
|
124
|
-
assert result is None
|
|
125
|
-
|
|
126
|
-
def test_returns_none_for_empty_config(self):
|
|
127
|
-
workspace_config = {"dependencies": {"merged": []}}
|
|
128
|
-
|
|
129
|
-
result = resolve_requirements(None, workspace_config)
|
|
130
|
-
|
|
131
|
-
assert result is None
|
|
132
|
-
|
|
133
|
-
def test_returns_none_for_missing_deps(self):
|
|
134
|
-
workspace_config = {"other_key": "value"}
|
|
135
|
-
|
|
136
|
-
result = resolve_requirements(None, workspace_config)
|
|
137
|
-
|
|
138
|
-
assert result is None
|
|
139
|
-
|
|
140
|
-
def test_explicit_takes_precedence(self, tmp_path):
|
|
141
|
-
req_file = tmp_path / "requirements.txt"
|
|
142
|
-
req_file.write_text("explicit==1.0.0")
|
|
143
|
-
|
|
144
|
-
workspace_config = {"dependencies": {"merged": ["config==2.0.0"]}}
|
|
145
|
-
|
|
146
|
-
result = resolve_requirements(str(req_file), workspace_config)
|
|
147
|
-
|
|
148
|
-
assert "explicit==1.0.0" in result
|
|
149
|
-
assert "config==2.0.0" not in result
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class TestResolveImportFiles:
|
|
153
|
-
"""Tests for resolve_import_files function."""
|
|
154
|
-
|
|
155
|
-
def test_resolves_local_imports(self, sample_workspace):
|
|
156
|
-
sys.path.insert(0, str(sample_workspace))
|
|
157
|
-
try:
|
|
158
|
-
main_file = sample_workspace / "main.py"
|
|
159
|
-
result = resolve_import_files(main_file, None)
|
|
160
|
-
|
|
161
|
-
assert result is not None
|
|
162
|
-
parsed = json.loads(result)
|
|
163
|
-
|
|
164
|
-
# Should contain package files
|
|
165
|
-
keys = list(parsed.keys())
|
|
166
|
-
assert len(keys) > 0
|
|
167
|
-
finally:
|
|
168
|
-
sys.path.remove(str(sample_workspace))
|
|
169
|
-
|
|
170
|
-
def test_returns_none_for_no_file(self):
|
|
171
|
-
result = resolve_import_files(None, None)
|
|
172
|
-
|
|
173
|
-
assert result is None
|
|
174
|
-
|
|
175
|
-
def test_returns_none_for_nonexistent_file(self, tmp_path):
|
|
176
|
-
nonexistent = tmp_path / "nonexistent.py"
|
|
177
|
-
|
|
178
|
-
result = resolve_import_files(nonexistent, None)
|
|
179
|
-
|
|
180
|
-
assert result is None
|
|
181
|
-
|
|
182
|
-
def test_returns_none_for_no_imports(self, tmp_path):
|
|
183
|
-
simple_file = tmp_path / "simple.py"
|
|
184
|
-
simple_file.write_text("print('hello')")
|
|
185
|
-
|
|
186
|
-
result = resolve_import_files(simple_file, None)
|
|
187
|
-
|
|
188
|
-
# No local imports, so returns None
|
|
189
|
-
assert result is None
|
|
190
|
-
|
|
191
|
-
def test_uses_workspace_config_root(self, sample_workspace, sample_workspace_config):
|
|
192
|
-
sample_workspace_config()
|
|
193
|
-
|
|
194
|
-
sys.path.insert(0, str(sample_workspace))
|
|
195
|
-
try:
|
|
196
|
-
main_file = sample_workspace / "main.py"
|
|
197
|
-
|
|
198
|
-
workspace_config = {
|
|
199
|
-
"_config_dir": sample_workspace,
|
|
200
|
-
"dependencies": {"merged": []},
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
result = resolve_import_files(main_file, workspace_config)
|
|
204
|
-
|
|
205
|
-
assert result is not None
|
|
206
|
-
finally:
|
|
207
|
-
sys.path.remove(str(sample_workspace))
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
class TestBuildPayload:
|
|
211
|
-
"""Tests for build_payload function."""
|
|
212
|
-
|
|
213
|
-
def test_minimal_payload(self):
|
|
214
|
-
payload = build_payload(code="print('hello')", machine_type="cpu")
|
|
215
|
-
|
|
216
|
-
assert payload["code"] == "print('hello')"
|
|
217
|
-
assert payload["execution_type"] == "cpu"
|
|
218
|
-
assert "timeout" in payload
|
|
219
|
-
assert payload["nbcode"] == 0
|
|
220
|
-
|
|
221
|
-
def test_full_payload(self):
|
|
222
|
-
payload = build_payload(
|
|
223
|
-
code="print('hello')",
|
|
224
|
-
machine_type="a100",
|
|
225
|
-
file_name="test.py",
|
|
226
|
-
requirements_content="numpy>=1.0",
|
|
227
|
-
imports=["os", "sys"],
|
|
228
|
-
import_files='{"test.py": "code"}',
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
assert payload["execution_type"] == "a100"
|
|
232
|
-
assert payload["file_name"] == "test.py"
|
|
233
|
-
assert payload["requirements_content"] == "numpy>=1.0"
|
|
234
|
-
assert payload["prior_imports"] == ["os", "sys"]
|
|
235
|
-
assert payload["import_files"] == '{"test.py": "code"}'
|
|
236
|
-
|
|
237
|
-
def test_omits_none_values(self):
|
|
238
|
-
payload = build_payload(
|
|
239
|
-
code="print('hello')",
|
|
240
|
-
machine_type="cpu",
|
|
241
|
-
file_name=None,
|
|
242
|
-
requirements_content=None,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
assert "file_name" not in payload
|
|
246
|
-
assert "requirements_content" not in payload
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class TestSubmitExecution:
|
|
250
|
-
"""Tests for submit_execution function."""
|
|
251
|
-
|
|
252
|
-
@patch("lyceum.external.compute.execution.python.httpx.post")
|
|
253
|
-
@patch("lyceum.external.compute.execution.python.config")
|
|
254
|
-
def test_success(self, mock_config, mock_post, setup_httpx_post, mock_execution_id):
|
|
255
|
-
mock_config.base_url = "https://api.lyceum.dev"
|
|
256
|
-
mock_config.api_key = "test-key"
|
|
257
|
-
mock_post.return_value = setup_httpx_post(execution_id=mock_execution_id)
|
|
258
|
-
|
|
259
|
-
exec_id, stream_url = submit_execution({"code": "print('hi')"})
|
|
260
|
-
|
|
261
|
-
assert exec_id == mock_execution_id
|
|
262
|
-
assert stream_url is not None
|
|
263
|
-
|
|
264
|
-
@patch("lyceum.external.compute.execution.python.httpx.post")
|
|
265
|
-
@patch("lyceum.external.compute.execution.python.config")
|
|
266
|
-
def test_auth_error(self, mock_config, mock_post, setup_httpx_response):
|
|
267
|
-
mock_config.base_url = "https://api.lyceum.dev"
|
|
268
|
-
mock_config.api_key = "invalid-key"
|
|
269
|
-
mock_post.return_value = setup_httpx_response(status_code=401, text="Unauthorized")
|
|
270
|
-
|
|
271
|
-
with pytest.raises(ClickExit):
|
|
272
|
-
submit_execution({"code": "print('hi')"})
|
|
273
|
-
|
|
274
|
-
@patch("lyceum.external.compute.execution.python.httpx.post")
|
|
275
|
-
@patch("lyceum.external.compute.execution.python.config")
|
|
276
|
-
def test_server_error(self, mock_config, mock_post, setup_httpx_response):
|
|
277
|
-
mock_config.base_url = "https://api.lyceum.dev"
|
|
278
|
-
mock_config.api_key = "test-key"
|
|
279
|
-
mock_post.return_value = setup_httpx_response(status_code=500, text="Server Error")
|
|
280
|
-
|
|
281
|
-
with pytest.raises(ClickExit):
|
|
282
|
-
submit_execution({"code": "print('hi')"})
|
|
283
|
-
|
|
284
|
-
@patch("lyceum.external.compute.execution.python.httpx.post")
|
|
285
|
-
@patch("lyceum.external.compute.execution.python.config")
|
|
286
|
-
def test_with_status_line(self, mock_config, mock_post, setup_httpx_post, mock_execution_id):
|
|
287
|
-
mock_config.base_url = "https://api.lyceum.dev"
|
|
288
|
-
mock_config.api_key = "test-key"
|
|
289
|
-
mock_post.return_value = setup_httpx_post(execution_id=mock_execution_id)
|
|
290
|
-
|
|
291
|
-
mock_status = MagicMock()
|
|
292
|
-
exec_id, stream_url = submit_execution({"code": "print('hi')"}, status=mock_status)
|
|
293
|
-
|
|
294
|
-
mock_status.update.assert_called_once_with("Submitting execution...")
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
class TestValidateMachineType:
|
|
298
|
-
"""Tests for validate_machine_type function."""
|
|
299
|
-
|
|
300
|
-
@patch("lyceum.external.compute.execution.python.get_available_machines")
|
|
301
|
-
def test_valid_machine(self, mock_get_machines):
|
|
302
|
-
mock_get_machines.return_value = ["cpu", "a100", "h100"]
|
|
303
|
-
|
|
304
|
-
assert validate_machine_type("cpu") is True
|
|
305
|
-
assert validate_machine_type("a100") is True
|
|
306
|
-
|
|
307
|
-
@patch("lyceum.external.compute.execution.python.get_available_machines")
|
|
308
|
-
def test_invalid_machine(self, mock_get_machines):
|
|
309
|
-
mock_get_machines.return_value = ["cpu"]
|
|
310
|
-
|
|
311
|
-
assert validate_machine_type("a100") is False
|
|
312
|
-
|
|
313
|
-
@patch("lyceum.external.compute.execution.python.get_available_machines")
|
|
314
|
-
def test_empty_available_returns_true(self, mock_get_machines):
|
|
315
|
-
# When we can't fetch available machines, assume valid
|
|
316
|
-
mock_get_machines.return_value = []
|
|
317
|
-
|
|
318
|
-
assert validate_machine_type("any_machine") is True
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
class TestLoadWorkspaceConfig:
|
|
322
|
-
"""Tests for load_workspace_config function."""
|
|
323
|
-
|
|
324
|
-
def test_finds_config_in_workspace(self, sample_workspace, sample_workspace_config):
|
|
325
|
-
sample_workspace_config()
|
|
326
|
-
|
|
327
|
-
main_file = sample_workspace / "main.py"
|
|
328
|
-
|
|
329
|
-
# Change cwd to tmp to avoid picking up real config
|
|
330
|
-
original_cwd = os.getcwd()
|
|
331
|
-
try:
|
|
332
|
-
os.chdir(sample_workspace)
|
|
333
|
-
config = load_workspace_config(main_file)
|
|
334
|
-
finally:
|
|
335
|
-
os.chdir(original_cwd)
|
|
336
|
-
|
|
337
|
-
assert config is not None
|
|
338
|
-
assert "dependencies" in config
|
|
339
|
-
|
|
340
|
-
def test_returns_none_without_config(self, tmp_path):
|
|
341
|
-
# Create a completely isolated workspace with no .lyceum config
|
|
342
|
-
isolated = tmp_path / "isolated_workspace"
|
|
343
|
-
isolated.mkdir()
|
|
344
|
-
main_file = isolated / "main.py"
|
|
345
|
-
main_file.write_text("print('hello')")
|
|
346
|
-
|
|
347
|
-
# Change cwd to the isolated dir to avoid picking up real config
|
|
348
|
-
original_cwd = os.getcwd()
|
|
349
|
-
try:
|
|
350
|
-
os.chdir(isolated)
|
|
351
|
-
config = load_workspace_config(main_file)
|
|
352
|
-
finally:
|
|
353
|
-
os.chdir(original_cwd)
|
|
354
|
-
|
|
355
|
-
assert config is None
|
|
356
|
-
|
|
357
|
-
def test_searches_parent_directories(self, sample_workspace, sample_workspace_config):
|
|
358
|
-
sample_workspace_config()
|
|
359
|
-
|
|
360
|
-
# Create a nested file
|
|
361
|
-
nested_dir = sample_workspace / "nested" / "deep"
|
|
362
|
-
nested_dir.mkdir(parents=True)
|
|
363
|
-
nested_file = nested_dir / "script.py"
|
|
364
|
-
nested_file.write_text("print('hi')")
|
|
365
|
-
|
|
366
|
-
original_cwd = os.getcwd()
|
|
367
|
-
try:
|
|
368
|
-
os.chdir(sample_workspace)
|
|
369
|
-
config = load_workspace_config(nested_file)
|
|
370
|
-
finally:
|
|
371
|
-
os.chdir(original_cwd)
|
|
372
|
-
|
|
373
|
-
assert config is not None
|
|
374
|
-
assert "_config_dir" in config
|
|
375
|
-
|
|
376
|
-
def test_returns_none_for_nonexistent_file(self, tmp_path):
|
|
377
|
-
# Create an isolated directory
|
|
378
|
-
isolated = tmp_path / "isolated"
|
|
379
|
-
isolated.mkdir()
|
|
380
|
-
nonexistent = isolated / "nonexistent.py"
|
|
381
|
-
|
|
382
|
-
original_cwd = os.getcwd()
|
|
383
|
-
try:
|
|
384
|
-
os.chdir(isolated)
|
|
385
|
-
config = load_workspace_config(nonexistent)
|
|
386
|
-
finally:
|
|
387
|
-
os.chdir(original_cwd)
|
|
388
|
-
|
|
389
|
-
# Should not crash, returns None
|
|
390
|
-
assert config is None
|
|
391
|
-
|
|
392
|
-
def test_adds_config_dir_to_result(self, sample_workspace, sample_workspace_config):
|
|
393
|
-
sample_workspace_config()
|
|
394
|
-
|
|
395
|
-
main_file = sample_workspace / "main.py"
|
|
396
|
-
|
|
397
|
-
original_cwd = os.getcwd()
|
|
398
|
-
try:
|
|
399
|
-
os.chdir(sample_workspace)
|
|
400
|
-
config = load_workspace_config(main_file)
|
|
401
|
-
finally:
|
|
402
|
-
os.chdir(original_cwd)
|
|
403
|
-
|
|
404
|
-
assert config is not None
|
|
405
|
-
assert "_config_dir" in config
|
|
406
|
-
assert config["_config_dir"] == sample_workspace
|