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.
Files changed (34) hide show
  1. lyceum/external/auth/login.py +18 -18
  2. lyceum/external/compute/execution/docker.py +4 -2
  3. lyceum/external/compute/execution/docker_compose.py +263 -0
  4. lyceum/external/compute/execution/notebook.py +0 -2
  5. lyceum/external/compute/execution/python.py +2 -1
  6. lyceum/external/compute/inference/batch.py +8 -10
  7. lyceum/external/vms/instances.py +301 -0
  8. lyceum/external/vms/management.py +383 -0
  9. lyceum/main.py +3 -0
  10. lyceum/shared/config.py +19 -24
  11. lyceum/shared/display.py +12 -31
  12. lyceum/shared/streaming.py +17 -45
  13. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/METADATA +1 -1
  14. lyceum_cli-1.0.27.dist-info/RECORD +34 -0
  15. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/WHEEL +1 -1
  16. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/top_level.txt +0 -1
  17. lyceum/external/compute/execution/docker_config.py +0 -123
  18. lyceum/external/storage/files.py +0 -273
  19. lyceum_cli-1.0.25.dist-info/RECORD +0 -46
  20. tests/__init__.py +0 -1
  21. tests/conftest.py +0 -200
  22. tests/unit/__init__.py +0 -1
  23. tests/unit/external/__init__.py +0 -1
  24. tests/unit/external/compute/__init__.py +0 -1
  25. tests/unit/external/compute/execution/__init__.py +0 -1
  26. tests/unit/external/compute/execution/test_data.py +0 -33
  27. tests/unit/external/compute/execution/test_dependency_resolver.py +0 -257
  28. tests/unit/external/compute/execution/test_python_helpers.py +0 -406
  29. tests/unit/external/compute/execution/test_python_run.py +0 -289
  30. tests/unit/shared/__init__.py +0 -1
  31. tests/unit/shared/test_config.py +0 -341
  32. tests/unit/shared/test_streaming.py +0 -259
  33. /lyceum/external/{storage → vms}/__init__.py +0 -0
  34. {lyceum_cli-1.0.25.dist-info → lyceum_cli-1.0.27.dist-info}/entry_points.txt +0 -0
@@ -1,289 +0,0 @@
1
- """Tests for run_python CLI command."""
2
-
3
- from unittest.mock import MagicMock, patch
4
-
5
- import pytest
6
- from typer.testing import CliRunner
7
-
8
- from lyceum.external.compute.execution.python import python_app
9
-
10
-
11
- class TestRunPythonCommand:
12
- """Tests for the run_python CLI command."""
13
-
14
- @pytest.fixture
15
- def cli_runner(self):
16
- return CliRunner()
17
-
18
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
19
- @patch("lyceum.external.compute.execution.python.submit_execution")
20
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
21
- @patch("lyceum.external.compute.execution.python.config")
22
- def test_run_inline_code(
23
- self,
24
- mock_config,
25
- mock_validate,
26
- mock_submit,
27
- mock_stream,
28
- cli_runner,
29
- mock_execution_id,
30
- ):
31
- mock_config.get_client.return_value = None
32
- mock_validate.return_value = True
33
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
34
- mock_stream.return_value = True
35
-
36
- result = cli_runner.invoke(python_app, ["run", "print('hello')"])
37
-
38
- assert result.exit_code == 0
39
- mock_submit.assert_called_once()
40
-
41
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
42
- @patch("lyceum.external.compute.execution.python.submit_execution")
43
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
44
- @patch("lyceum.external.compute.execution.python.config")
45
- def test_run_file(
46
- self,
47
- mock_config,
48
- mock_validate,
49
- mock_submit,
50
- mock_stream,
51
- cli_runner,
52
- sample_workspace,
53
- mock_execution_id,
54
- ):
55
- mock_config.get_client.return_value = None
56
- mock_validate.return_value = True
57
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
58
- mock_stream.return_value = True
59
-
60
- main_file = sample_workspace / "main.py"
61
- result = cli_runner.invoke(python_app, ["run", str(main_file)])
62
-
63
- assert result.exit_code == 0
64
-
65
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
66
- @patch("lyceum.external.compute.execution.python.config")
67
- def test_invalid_machine_type(
68
- self,
69
- mock_config,
70
- mock_validate,
71
- cli_runner,
72
- ):
73
- mock_config.get_client.return_value = None
74
- mock_validate.return_value = False
75
-
76
- result = cli_runner.invoke(python_app, ["run", "print('hi')", "-m", "invalid"])
77
-
78
- assert result.exit_code == 1
79
- assert "don't have access" in result.output
80
-
81
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
82
- @patch("lyceum.external.compute.execution.python.submit_execution")
83
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
84
- @patch("lyceum.external.compute.execution.python.config")
85
- def test_execution_failure(
86
- self,
87
- mock_config,
88
- mock_validate,
89
- mock_submit,
90
- mock_stream,
91
- cli_runner,
92
- mock_execution_id,
93
- ):
94
- mock_config.get_client.return_value = None
95
- mock_validate.return_value = True
96
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
97
- mock_stream.return_value = False # Execution failed
98
-
99
- result = cli_runner.invoke(python_app, ["run", "print('hi')"])
100
-
101
- assert result.exit_code == 1
102
-
103
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
104
- @patch("lyceum.external.compute.execution.python.submit_execution")
105
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
106
- @patch("lyceum.external.compute.execution.python.config")
107
- def test_machine_type_option(
108
- self,
109
- mock_config,
110
- mock_validate,
111
- mock_submit,
112
- mock_stream,
113
- cli_runner,
114
- mock_execution_id,
115
- ):
116
- mock_config.get_client.return_value = None
117
- mock_validate.return_value = True
118
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
119
- mock_stream.return_value = True
120
-
121
- result = cli_runner.invoke(python_app, ["run", "print('hi')", "-m", "a100"])
122
-
123
- assert result.exit_code == 0
124
- # Check that payload was built with correct machine type
125
- call_args = mock_submit.call_args[0][0]
126
- assert call_args["execution_type"] == "a100"
127
-
128
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
129
- @patch("lyceum.external.compute.execution.python.submit_execution")
130
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
131
- @patch("lyceum.external.compute.execution.python.config")
132
- def test_file_name_option(
133
- self,
134
- mock_config,
135
- mock_validate,
136
- mock_submit,
137
- mock_stream,
138
- cli_runner,
139
- mock_execution_id,
140
- ):
141
- mock_config.get_client.return_value = None
142
- mock_validate.return_value = True
143
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
144
- mock_stream.return_value = True
145
-
146
- result = cli_runner.invoke(
147
- python_app, ["run", "print('hi')", "-f", "custom_name.py"]
148
- )
149
-
150
- assert result.exit_code == 0
151
- call_args = mock_submit.call_args[0][0]
152
- assert call_args["file_name"] == "custom_name.py"
153
-
154
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
155
- @patch("lyceum.external.compute.execution.python.submit_execution")
156
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
157
- @patch("lyceum.external.compute.execution.python.config")
158
- def test_requirements_option(
159
- self,
160
- mock_config,
161
- mock_validate,
162
- mock_submit,
163
- mock_stream,
164
- cli_runner,
165
- mock_execution_id,
166
- ):
167
- mock_config.get_client.return_value = None
168
- mock_validate.return_value = True
169
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
170
- mock_stream.return_value = True
171
-
172
- result = cli_runner.invoke(
173
- python_app, ["run", "print('hi')", "-r", "numpy>=1.0"]
174
- )
175
-
176
- assert result.exit_code == 0
177
- call_args = mock_submit.call_args[0][0]
178
- assert call_args["requirements_content"] == "numpy>=1.0"
179
-
180
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
181
- @patch("lyceum.external.compute.execution.python.submit_execution")
182
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
183
- @patch("lyceum.external.compute.execution.python.config")
184
- def test_shows_execution_id(
185
- self,
186
- mock_config,
187
- mock_validate,
188
- mock_submit,
189
- mock_stream,
190
- cli_runner,
191
- mock_execution_id,
192
- ):
193
- mock_config.get_client.return_value = None
194
- mock_validate.return_value = True
195
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
196
- mock_stream.return_value = True
197
-
198
- result = cli_runner.invoke(python_app, ["run", "print('hi')"])
199
-
200
- assert result.exit_code == 0
201
- assert mock_execution_id in result.output
202
-
203
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
204
- @patch("lyceum.external.compute.execution.python.submit_execution")
205
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
206
- @patch("lyceum.external.compute.execution.python.load_workspace_config")
207
- @patch("lyceum.external.compute.execution.python.config")
208
- def test_no_config_option(
209
- self,
210
- mock_config,
211
- mock_load_workspace,
212
- mock_validate,
213
- mock_submit,
214
- mock_stream,
215
- cli_runner,
216
- mock_execution_id,
217
- ):
218
- mock_config.get_client.return_value = None
219
- mock_validate.return_value = True
220
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
221
- mock_stream.return_value = True
222
-
223
- result = cli_runner.invoke(
224
- python_app, ["run", "print('hi')", "--no-config"]
225
- )
226
-
227
- assert result.exit_code == 0
228
- mock_load_workspace.assert_not_called()
229
-
230
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
231
- @patch("lyceum.external.compute.execution.python.submit_execution")
232
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
233
- @patch("lyceum.external.compute.execution.python.config")
234
- def test_script_args_passed_through(
235
- self,
236
- mock_config,
237
- mock_validate,
238
- mock_submit,
239
- mock_stream,
240
- cli_runner,
241
- mock_execution_id,
242
- ):
243
- mock_config.get_client.return_value = None
244
- mock_validate.return_value = True
245
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
246
- mock_stream.return_value = True
247
-
248
- result = cli_runner.invoke(
249
- python_app, ["run", "print('hi')", "--", "--epochs", "10", "--lr", "0.001"]
250
- )
251
-
252
- assert result.exit_code == 0
253
- # Verify that the code was modified to include sys.argv
254
- call_args = mock_submit.call_args[0][0]
255
- assert "sys.argv" in call_args["code"]
256
- assert "--epochs" in call_args["code"]
257
- assert "10" in call_args["code"]
258
-
259
-
260
- class TestRunPythonWithDebug:
261
- """Tests for run_python command with debug flag."""
262
-
263
- @pytest.fixture
264
- def cli_runner(self):
265
- return CliRunner()
266
-
267
- @patch("lyceum.external.compute.execution.python.stream_execution_output")
268
- @patch("lyceum.external.compute.execution.python.submit_execution")
269
- @patch("lyceum.external.compute.execution.python.validate_machine_type")
270
- @patch("lyceum.external.compute.execution.python.config")
271
- def test_debug_flag(
272
- self,
273
- mock_config,
274
- mock_validate,
275
- mock_submit,
276
- mock_stream,
277
- cli_runner,
278
- mock_execution_id,
279
- ):
280
- mock_config.get_client.return_value = None
281
- mock_validate.return_value = True
282
- mock_submit.return_value = (mock_execution_id, "http://stream.url")
283
- mock_stream.return_value = True
284
-
285
- result = cli_runner.invoke(python_app, ["run", "print('hi')", "--debug"])
286
-
287
- assert result.exit_code == 0
288
- # Debug output should contain payload summary
289
- assert "DEBUG" in result.output or "Payload" in result.output or mock_execution_id in result.output
@@ -1 +0,0 @@
1
- """Unit tests for shared utilities."""
@@ -1,341 +0,0 @@
1
- """Tests for authentication config."""
2
-
3
- import json
4
- import time
5
- from pathlib import Path
6
- from unittest.mock import MagicMock, patch
7
-
8
- import jwt
9
- import pytest
10
- from click.exceptions import Exit as ClickExit
11
-
12
-
13
- class TestConfigLoad:
14
- """Tests for Config.load() method."""
15
-
16
- def test_load_existing_config(self, tmp_path):
17
- config_dir = tmp_path / ".lyceum"
18
- config_dir.mkdir(parents=True)
19
- config_file = config_dir / "config.json"
20
- config_data = {
21
- "api_key": "test-api-key",
22
- "refresh_token": "test-refresh-token",
23
- "base_url": "https://api.test.dev",
24
- }
25
- config_file.write_text(json.dumps(config_data))
26
-
27
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
28
- from lyceum.shared.config import Config
29
-
30
- config = Config()
31
-
32
- assert config.api_key == "test-api-key"
33
- assert config.refresh_token == "test-refresh-token"
34
- assert config.base_url == "https://api.test.dev"
35
-
36
- def test_load_missing_file(self, tmp_path):
37
- missing_path = tmp_path / ".lyceum" / "config.json"
38
-
39
- with patch("lyceum.shared.config.CONFIG_FILE", missing_path):
40
- from lyceum.shared.config import Config
41
-
42
- config = Config()
43
-
44
- assert config.api_key is None
45
- assert config.refresh_token is None
46
-
47
- def test_load_partial_config(self, tmp_path):
48
- config_dir = tmp_path / ".lyceum"
49
- config_dir.mkdir(parents=True)
50
- config_file = config_dir / "config.json"
51
- config_data = {"api_key": "only-api-key"}
52
- config_file.write_text(json.dumps(config_data))
53
-
54
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
55
- from lyceum.shared.config import Config
56
-
57
- config = Config()
58
-
59
- assert config.api_key == "only-api-key"
60
- assert config.refresh_token is None
61
-
62
- def test_load_malformed_json(self, tmp_path):
63
- config_dir = tmp_path / ".lyceum"
64
- config_dir.mkdir(parents=True)
65
- config_file = config_dir / "config.json"
66
- config_file.write_text("not valid json {{{")
67
-
68
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
69
- from lyceum.shared.config import Config
70
-
71
- config = Config()
72
- # Should not crash, just use defaults
73
- assert config.api_key is None
74
-
75
-
76
- class TestConfigSave:
77
- """Tests for Config.save() method."""
78
-
79
- def test_save_creates_directory(self, tmp_path):
80
- config_dir = tmp_path / ".lyceum"
81
- config_file = config_dir / "config.json"
82
-
83
- with (
84
- patch("lyceum.shared.config.CONFIG_DIR", config_dir),
85
- patch("lyceum.shared.config.CONFIG_FILE", config_file),
86
- ):
87
- from lyceum.shared.config import Config
88
-
89
- config = Config()
90
- config.api_key = "new-api-key"
91
- config.refresh_token = "new-refresh-token"
92
- config.save()
93
-
94
- assert config_dir.exists()
95
- assert config_file.exists()
96
-
97
- saved_data = json.loads(config_file.read_text())
98
- assert saved_data["api_key"] == "new-api-key"
99
- assert saved_data["refresh_token"] == "new-refresh-token"
100
-
101
-
102
- class TestConfigIsTokenExpired:
103
- """Tests for Config.is_token_expired() method."""
104
-
105
- def test_valid_token_not_expired(self, tmp_path):
106
- config_file = tmp_path / ".lyceum" / "config.json"
107
-
108
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
109
- from lyceum.shared.config import Config
110
-
111
- # Create token expiring in 1 hour
112
- future_exp = int(time.time()) + 3600
113
- token = jwt.encode({"exp": future_exp}, "secret", algorithm="HS256")
114
-
115
- config = Config()
116
- config.api_key = token
117
-
118
- assert config.is_token_expired() is False
119
-
120
- def test_expired_token(self, tmp_path):
121
- config_file = tmp_path / ".lyceum" / "config.json"
122
-
123
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
124
- from lyceum.shared.config import Config
125
-
126
- # Create token that expired 1 hour ago
127
- past_exp = int(time.time()) - 3600
128
- token = jwt.encode({"exp": past_exp}, "secret", algorithm="HS256")
129
-
130
- config = Config()
131
- config.api_key = token
132
-
133
- assert config.is_token_expired() is True
134
-
135
- def test_grace_period(self, tmp_path):
136
- config_file = tmp_path / ".lyceum" / "config.json"
137
-
138
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
139
- from lyceum.shared.config import Config
140
-
141
- # Create token expiring in 3 minutes (within 5-min grace period)
142
- near_exp = int(time.time()) + 180
143
- token = jwt.encode({"exp": near_exp}, "secret", algorithm="HS256")
144
-
145
- config = Config()
146
- config.api_key = token
147
-
148
- assert config.is_token_expired() is True
149
-
150
- def test_legacy_api_key_never_expired(self, tmp_path):
151
- config_file = tmp_path / ".lyceum" / "config.json"
152
-
153
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
154
- from lyceum.shared.config import Config
155
-
156
- config = Config()
157
- config.api_key = "lk_test_api_key_12345"
158
-
159
- assert config.is_token_expired() is False
160
-
161
- def test_missing_token(self, tmp_path):
162
- config_file = tmp_path / ".lyceum" / "config.json"
163
-
164
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
165
- from lyceum.shared.config import Config
166
-
167
- config = Config()
168
- config.api_key = None
169
-
170
- assert config.is_token_expired() is True
171
-
172
- def test_invalid_jwt_token(self, tmp_path):
173
- config_file = tmp_path / ".lyceum" / "config.json"
174
-
175
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
176
- from lyceum.shared.config import Config
177
-
178
- config = Config()
179
- config.api_key = "not-a-valid-jwt-token"
180
-
181
- # Invalid JWT should be treated as expired
182
- assert config.is_token_expired() is True
183
-
184
-
185
- class TestConfigRefreshToken:
186
- """Tests for Config.refresh_access_token() method."""
187
-
188
- def test_refresh_success(self, tmp_path):
189
- config_file = tmp_path / ".lyceum" / "config.json"
190
- config_dir = tmp_path / ".lyceum"
191
-
192
- with (
193
- patch("lyceum.shared.config.CONFIG_FILE", config_file),
194
- patch("lyceum.shared.config.CONFIG_DIR", config_dir),
195
- patch("lyceum.shared.config.create_client") as mock_create_client,
196
- ):
197
- mock_session = MagicMock()
198
- mock_session.access_token = "new-access-token"
199
- mock_session.refresh_token = "new-refresh-token"
200
-
201
- mock_response = MagicMock()
202
- mock_response.session = mock_session
203
-
204
- mock_auth = MagicMock()
205
- mock_auth.refresh_session.return_value = mock_response
206
-
207
- mock_client = MagicMock()
208
- mock_client.auth = mock_auth
209
- mock_create_client.return_value = mock_client
210
-
211
- from lyceum.shared.config import Config
212
-
213
- config = Config()
214
- config.refresh_token = "old-refresh-token"
215
-
216
- result = config.refresh_access_token()
217
-
218
- assert result is True
219
- assert config.api_key == "new-access-token"
220
- assert config.refresh_token == "new-refresh-token"
221
-
222
- def test_refresh_failure_no_session(self, tmp_path):
223
- config_file = tmp_path / ".lyceum" / "config.json"
224
-
225
- with (
226
- patch("lyceum.shared.config.CONFIG_FILE", config_file),
227
- patch("lyceum.shared.config.create_client") as mock_create_client,
228
- ):
229
- mock_response = MagicMock()
230
- mock_response.session = None
231
-
232
- mock_auth = MagicMock()
233
- mock_auth.refresh_session.return_value = mock_response
234
-
235
- mock_client = MagicMock()
236
- mock_client.auth = mock_auth
237
- mock_create_client.return_value = mock_client
238
-
239
- from lyceum.shared.config import Config
240
-
241
- config = Config()
242
- config.refresh_token = "invalid-token"
243
-
244
- result = config.refresh_access_token()
245
-
246
- assert result is False
247
-
248
- def test_refresh_failure_no_refresh_token(self, tmp_path):
249
- config_file = tmp_path / ".lyceum" / "config.json"
250
-
251
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
252
- from lyceum.shared.config import Config
253
-
254
- config = Config()
255
- config.refresh_token = None
256
-
257
- result = config.refresh_access_token()
258
-
259
- assert result is False
260
-
261
- def test_refresh_skipped_for_legacy_key(self, tmp_path):
262
- config_file = tmp_path / ".lyceum" / "config.json"
263
-
264
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
265
- from lyceum.shared.config import Config
266
-
267
- config = Config()
268
- config.api_key = "lk_test_api_key"
269
- config.refresh_token = "some-refresh-token"
270
-
271
- result = config.refresh_access_token()
272
-
273
- assert result is False
274
-
275
- def test_refresh_exception_handling(self, tmp_path):
276
- config_file = tmp_path / ".lyceum" / "config.json"
277
-
278
- with (
279
- patch("lyceum.shared.config.CONFIG_FILE", config_file),
280
- patch("lyceum.shared.config.create_client") as mock_create_client,
281
- ):
282
- mock_create_client.side_effect = Exception("Connection error")
283
-
284
- from lyceum.shared.config import Config
285
-
286
- config = Config()
287
- config.refresh_token = "some-token"
288
-
289
- result = config.refresh_access_token()
290
-
291
- assert result is False
292
-
293
-
294
- class TestConfigGetClient:
295
- """Tests for Config.get_client() method."""
296
-
297
- def test_get_client_no_api_key(self, tmp_path):
298
- config_file = tmp_path / ".lyceum" / "config.json"
299
-
300
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
301
- from lyceum.shared.config import Config
302
-
303
- config = Config()
304
- config.api_key = None
305
-
306
- with pytest.raises(ClickExit):
307
- config.get_client()
308
-
309
- def test_get_client_expired_token_refresh_fails(self, tmp_path):
310
- config_file = tmp_path / ".lyceum" / "config.json"
311
-
312
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
313
- from lyceum.shared.config import Config
314
-
315
- # Create expired token
316
- past_exp = int(time.time()) - 3600
317
- token = jwt.encode({"exp": past_exp}, "secret", algorithm="HS256")
318
-
319
- config = Config()
320
- config.api_key = token
321
- config.refresh_token = None # No refresh token
322
-
323
- with pytest.raises(ClickExit):
324
- config.get_client()
325
-
326
- def test_get_client_valid_token(self, tmp_path):
327
- config_file = tmp_path / ".lyceum" / "config.json"
328
-
329
- with patch("lyceum.shared.config.CONFIG_FILE", config_file):
330
- from lyceum.shared.config import Config
331
-
332
- # Create token expiring in 1 hour
333
- future_exp = int(time.time()) + 3600
334
- token = jwt.encode({"exp": future_exp}, "secret", algorithm="HS256")
335
-
336
- config = Config()
337
- config.api_key = token
338
-
339
- result = config.get_client()
340
-
341
- assert result is config