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,273 +0,0 @@
1
- """Storage file management commands"""
2
-
3
- from datetime import datetime
4
- from pathlib import Path
5
-
6
- import httpx
7
- import typer
8
- from rich.console import Console
9
- from rich.table import Table
10
-
11
- from ...shared.config import config
12
-
13
- console = Console()
14
-
15
- storage_app = typer.Typer(name="storage", help="File storage commands")
16
-
17
-
18
- def format_size(size_bytes: int) -> str:
19
- """Format bytes into human-readable size."""
20
- for unit in ["B", "KB", "MB", "GB"]:
21
- if size_bytes < 1024:
22
- return f"{size_bytes:.1f} {unit}"
23
- size_bytes /= 1024
24
- return f"{size_bytes:.1f} TB"
25
-
26
-
27
- @storage_app.command("list")
28
- def list_files(
29
- prefix: str = typer.Option("", "--prefix", "-p", help="Filter by prefix/folder"),
30
- limit: int = typer.Option(100, "--limit", "-n", help="Maximum files to list"),
31
- ):
32
- """List files in your storage bucket."""
33
- try:
34
- config.get_client()
35
-
36
- params = {"prefix": prefix, "max_files": limit}
37
-
38
- response = httpx.get(
39
- f"{config.base_url}/api/v2/external/storage/list-files",
40
- headers={"Authorization": f"Bearer {config.api_key}"},
41
- params=params,
42
- timeout=30.0,
43
- )
44
-
45
- if response.status_code != 200:
46
- console.print(f"[red]Error: HTTP {response.status_code}[/red]")
47
- if response.status_code == 401:
48
- console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
49
- else:
50
- console.print(f"[red]{response.content.decode()}[/red]")
51
- raise typer.Exit(1)
52
-
53
- files = response.json()
54
-
55
- if not files:
56
- console.print("[dim]No files found.[/dim]")
57
- if prefix:
58
- console.print(f"[dim]Prefix filter: {prefix}[/dim]")
59
- return
60
-
61
- table = Table(title="Storage Files")
62
- table.add_column("Key", style="cyan")
63
- table.add_column("Size", style="green", justify="right")
64
- table.add_column("Last Modified", style="dim")
65
-
66
- for f in files:
67
- last_mod = f.get("last_modified", "")
68
- if last_mod:
69
- try:
70
- dt = datetime.fromisoformat(last_mod.replace("Z", "+00:00"))
71
- last_mod = dt.strftime("%Y-%m-%d %H:%M")
72
- except Exception:
73
- pass
74
-
75
- table.add_row(
76
- f.get("key", ""),
77
- format_size(f.get("size", 0)),
78
- last_mod,
79
- )
80
-
81
- console.print(table)
82
- console.print(f"\n[dim]Total: {len(files)} file(s)[/dim]")
83
-
84
- except typer.Exit:
85
- raise
86
- except Exception as e:
87
- console.print(f"[red]Error: {e}[/red]")
88
- raise typer.Exit(1)
89
-
90
-
91
- @storage_app.command("upload")
92
- def upload_file(
93
- file_path: Path = typer.Argument(..., help="Local file to upload"),
94
- dest: str = typer.Option(None, "--dest", "-d", help="Destination path in storage (defaults to filename)"),
95
- ):
96
- """Upload a file to your storage bucket."""
97
- try:
98
- config.get_client()
99
-
100
- if not file_path.exists():
101
- console.print(f"[red]Error: File not found: {file_path}[/red]")
102
- raise typer.Exit(1)
103
-
104
- if not file_path.is_file():
105
- console.print(f"[red]Error: Not a file: {file_path}[/red]")
106
- raise typer.Exit(1)
107
-
108
- remote_path = dest or file_path.name
109
- file_size = file_path.stat().st_size
110
-
111
- console.print(f"[dim]Uploading {file_path.name} ({format_size(file_size)})...[/dim]")
112
-
113
- with open(file_path, "rb") as f:
114
- files = {"file": (file_path.name, f)}
115
- data = {"key": remote_path} if dest else {}
116
-
117
- response = httpx.post(
118
- f"{config.base_url}/api/v2/external/storage/upload",
119
- headers={"Authorization": f"Bearer {config.api_key}"},
120
- files=files,
121
- data=data,
122
- timeout=300.0, # 5 min timeout for large files
123
- )
124
-
125
- if response.status_code != 200:
126
- console.print(f"[red]Error: HTTP {response.status_code}[/red]")
127
- if response.status_code == 401:
128
- console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
129
- else:
130
- console.print(f"[red]{response.content.decode()}[/red]")
131
- raise typer.Exit(1)
132
-
133
- result = response.json()
134
- console.print(f"[green]Uploaded successfully![/green]")
135
- console.print(f"[dim]Key: {result.get('key')}[/dim]")
136
- console.print(f"[dim]Size: {format_size(result.get('size', 0))}[/dim]")
137
-
138
- except typer.Exit:
139
- raise
140
- except Exception as e:
141
- console.print(f"[red]Error: {e}[/red]")
142
- raise typer.Exit(1)
143
-
144
-
145
- @storage_app.command("download")
146
- def download_file(
147
- path: str = typer.Argument(..., help="File path in storage to download"),
148
- output: Path = typer.Option(None, "--output", "-o", help="Local output path (defaults to filename)"),
149
- ):
150
- """Download a file from your storage bucket."""
151
- try:
152
- config.get_client()
153
-
154
- # Default output to the filename part of the path
155
- output_path = output or Path(path.split("/")[-1])
156
-
157
- console.print(f"[dim]Downloading {path}...[/dim]")
158
-
159
- response = httpx.get(
160
- f"{config.base_url}/api/v2/external/storage/download/{path}",
161
- headers={"Authorization": f"Bearer {config.api_key}"},
162
- timeout=300.0,
163
- )
164
-
165
- if response.status_code == 404:
166
- console.print(f"[red]Error: File not found: {path}[/red]")
167
- raise typer.Exit(1)
168
-
169
- if response.status_code != 200:
170
- console.print(f"[red]Error: HTTP {response.status_code}[/red]")
171
- if response.status_code == 401:
172
- console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
173
- else:
174
- console.print(f"[red]{response.content.decode()}[/red]")
175
- raise typer.Exit(1)
176
-
177
- with open(output_path, "wb") as f:
178
- f.write(response.content)
179
-
180
- console.print(f"[green]Downloaded successfully![/green]")
181
- console.print(f"[dim]Saved to: {output_path}[/dim]")
182
- console.print(f"[dim]Size: {format_size(len(response.content))}[/dim]")
183
-
184
- except typer.Exit:
185
- raise
186
- except Exception as e:
187
- console.print(f"[red]Error: {e}[/red]")
188
- raise typer.Exit(1)
189
-
190
-
191
- @storage_app.command("delete")
192
- def delete_file(
193
- path: str = typer.Argument(..., help="File path in storage to delete"),
194
- force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
195
- ):
196
- """Delete a file from your storage bucket."""
197
- try:
198
- config.get_client()
199
-
200
- if not force:
201
- confirm = typer.confirm(f"Delete '{path}'?")
202
- if not confirm:
203
- console.print("[dim]Cancelled.[/dim]")
204
- raise typer.Exit(0)
205
-
206
- response = httpx.delete(
207
- f"{config.base_url}/api/v2/external/storage/delete/{path}",
208
- headers={"Authorization": f"Bearer {config.api_key}"},
209
- timeout=30.0,
210
- )
211
-
212
- if response.status_code == 404:
213
- console.print(f"[red]Error: File not found: {path}[/red]")
214
- raise typer.Exit(1)
215
-
216
- if response.status_code != 200:
217
- console.print(f"[red]Error: HTTP {response.status_code}[/red]")
218
- if response.status_code == 401:
219
- console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
220
- else:
221
- console.print(f"[red]{response.content.decode()}[/red]")
222
- raise typer.Exit(1)
223
-
224
- console.print(f"[green]Deleted: {path}[/green]")
225
-
226
- except typer.Exit:
227
- raise
228
- except Exception as e:
229
- console.print(f"[red]Error: {e}[/red]")
230
- raise typer.Exit(1)
231
-
232
-
233
- @storage_app.command("delete-folder")
234
- def delete_folder(
235
- prefix: str = typer.Argument(..., help="Folder prefix to delete"),
236
- force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
237
- ):
238
- """Delete all files in a folder (by prefix)."""
239
- try:
240
- config.get_client()
241
-
242
- if not force:
243
- confirm = typer.confirm(f"Delete all files in '{prefix}/'?")
244
- if not confirm:
245
- console.print("[dim]Cancelled.[/dim]")
246
- raise typer.Exit(0)
247
-
248
- response = httpx.delete(
249
- f"{config.base_url}/api/v2/external/storage/delete-folder/{prefix}",
250
- headers={"Authorization": f"Bearer {config.api_key}"},
251
- timeout=60.0,
252
- )
253
-
254
- if response.status_code == 404:
255
- console.print(f"[yellow]No files found in folder: {prefix}/[/yellow]")
256
- raise typer.Exit(0)
257
-
258
- if response.status_code != 200:
259
- console.print(f"[red]Error: HTTP {response.status_code}[/red]")
260
- if response.status_code == 401:
261
- console.print("[yellow]Run 'lyceum auth login' to re-authenticate.[/yellow]")
262
- else:
263
- console.print(f"[red]{response.content.decode()}[/red]")
264
- raise typer.Exit(1)
265
-
266
- result = response.json()
267
- console.print(f"[green]{result.get('message')}[/green]")
268
-
269
- except typer.Exit:
270
- raise
271
- except Exception as e:
272
- console.print(f"[red]Error: {e}[/red]")
273
- raise typer.Exit(1)
@@ -1,46 +0,0 @@
1
- lyceum/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
- lyceum/main.py,sha256=jE4P4SvHTeguLKL9wRBfOBxGHzDKxH_PcM46ouY-xnY,921
3
- lyceum/external/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
- lyceum/external/auth/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
- lyceum/external/auth/login.py,sha256=T-V6pzi3s0ZynpUUeLnN2y-cxJI4msj5z26S3_wJ1AE,23526
6
- lyceum/external/compute/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
- lyceum/external/compute/execution/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
- lyceum/external/compute/execution/config.py,sha256=6JJgLJnDPTwevEaNdB1nEICih_qbBmws5u5_S9gj7k0,8866
9
- lyceum/external/compute/execution/docker.py,sha256=lATtJzsmsu9iCUH-ynXZsfrIg-_aCkYE5r2l3DolisQ,9603
10
- lyceum/external/compute/execution/docker_config.py,sha256=tIiEVlp3yYZxLqhRsBqri7wV9_K0fPm6bvTRhRnKEd4,4071
11
- lyceum/external/compute/execution/notebook.py,sha256=fbN546-j1Cw88j7jbDKkup8J1xaws0eBYZJgt1NTJVg,7544
12
- lyceum/external/compute/execution/python.py,sha256=1ekNuF8_j-YG0-oNFP3C6Jjyw7IUlIngvL06C1usyu8,12864
13
- lyceum/external/compute/execution/workloads.py,sha256=4fsRWbYGmsQMGPPIN1jUG8cG5NPG9yV26ANJ-DtaXqc,5844
14
- lyceum/external/compute/inference/__init__.py,sha256=4YLoUKDEzitexynJv_Q5O0w1lty8CJ6uyRxuc1LiaBw,89
15
- lyceum/external/compute/inference/batch.py,sha256=HWwS6XHDZOG7QftNTNGvzao8Y6DzkEOdpOAZAEZyav4,11438
16
- lyceum/external/compute/inference/chat.py,sha256=hITj_UGLaxCJQskU-YbeaEerM5Xt_eJpEsYrTJoUpk4,8485
17
- lyceum/external/compute/inference/models.py,sha256=BkCEdvyliezGOUulj557e-Eoif0_HKR3CxqpEhdAZaA,10339
18
- lyceum/external/general/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
19
- lyceum/external/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- lyceum/external/storage/files.py,sha256=WdSp6t90jLzChOkg2uI4rZ5pq6bmHDCSo7CDUDVXWyk,9311
21
- lyceum/shared/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
22
- lyceum/shared/config.py,sha256=mZXbdwKxbkRA6RCnTPruaNWtLOaheYO4MnCxXTFNLu8,5548
23
- lyceum/shared/display.py,sha256=n9QQy-JVvoAgzss9D9cGfdw7GUY6t9qTYWr81v9q1fk,4855
24
- lyceum/shared/imports.py,sha256=wEG4wfVTIqJ6MBWDRAN96iGmVCb9ST2aOqSjkbvajug,11768
25
- lyceum/shared/streaming.py,sha256=wFb7w7fra63y8WWaIA8_E1Z6Sx_6G-0J53Zh010eZgk,9355
26
- lyceum_cloud_execution_api_client/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
27
- lyceum_cloud_execution_api_client/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
- lyceum_cloud_execution_api_client/models/__init__.py,sha256=AMlb9R9O9aNC9hvKz_8TFpEfOolYC3VtFS5JX17kYks,4888
29
- tests/__init__.py,sha256=yuZY5OKj5n7_jxuEuXtUjl77XCIopOxfiz7s_leAYPc,24
30
- tests/conftest.py,sha256=TwnnolWULDaAyCygQLj595TKreS1p2y-ezeN0b2WWX8,5052
31
- tests/unit/__init__.py,sha256=_w8WnsodwDTzGEx1Xtj1xjihh_VCUevi4LA4tOdW_Uw,33
32
- tests/unit/external/__init__.py,sha256=7lGFpul2lNMPJVPV1Jd27wzpsgb9EZwwa6q8fixUFLY,39
33
- tests/unit/external/compute/__init__.py,sha256=hf16EmvKpT4P0iw292Z0xCRswKqcU8PteT4Zr1sgRLo,38
34
- tests/unit/external/compute/execution/__init__.py,sha256=eUpROWmXCsK9ZaImYMw9iIDiQUIxj5HCrd06SyjE0Vg,40
35
- tests/unit/external/compute/execution/test_data.py,sha256=49p19xquOlmm7jKwCr6NQLooG9MpK8GkGQ3PwHrVuj0,856
36
- tests/unit/external/compute/execution/test_dependency_resolver.py,sha256=2X0Lan8C4av7QDeHpjZxk_RlTB1C6GzU1Gv5G0WYnSg,9479
37
- tests/unit/external/compute/execution/test_python_helpers.py,sha256=KyeTX0ps7L0x7xBSslwiQHiGa3oriX_vcqgkcJvbcm4,13421
38
- tests/unit/external/compute/execution/test_python_run.py,sha256=fKuWyTVshC3iGvoWfQmeNMi0N-46iCC_Zm2-lq-n-Ro,10353
39
- tests/unit/shared/__init__.py,sha256=ouZueovCsCdgfuKoMP1Uh69E3fmn0U8ltllHWhXhpgg,39
40
- tests/unit/shared/test_config.py,sha256=rokbpv_S9J-aUtg41eddn0ZuVWyTYZNXO9VtCZdXstU,11315
41
- tests/unit/shared/test_streaming.py,sha256=sFjkla5SvaWWPi42lQ6vHSTor_htLMMbBttnCyGsB98,8706
42
- lyceum_cli-1.0.25.dist-info/METADATA,sha256=mC4h00m-5ihgHqd9gdNoIpET5Ok0EQv5cAqLKaVLc2c,1482
43
- lyceum_cli-1.0.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
- lyceum_cli-1.0.25.dist-info/entry_points.txt,sha256=Oq-9wDkxVd6MHgNiUTYwXI9SGhvR3VkD7Mvk0xhiUZo,43
45
- lyceum_cli-1.0.25.dist-info/top_level.txt,sha256=-546wowhLvi8w6Gef9vwO0T1i0ME2PanTrgC0YX29y4,47
46
- lyceum_cli-1.0.25.dist-info/RECORD,,
tests/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Lyceum CLI tests."""
tests/conftest.py DELETED
@@ -1,200 +0,0 @@
1
- """Shared pytest fixtures for CLI tests."""
2
-
3
- import json
4
- import os
5
- from pathlib import Path
6
- from unittest.mock import AsyncMock, MagicMock, Mock, patch
7
-
8
- import pytest
9
-
10
-
11
- # --- Basic Fixtures ---
12
-
13
-
14
- @pytest.fixture
15
- def mock_execution_id() -> str:
16
- """Consistent execution ID for testing."""
17
- return "550e8400-e29b-41d4-a716-446655440000"
18
-
19
-
20
- @pytest.fixture
21
- def mock_streaming_url(mock_execution_id) -> str:
22
- """Mock streaming URL."""
23
- return f"https://api.lyceum.dev/stream/{mock_execution_id}"
24
-
25
-
26
- # --- Config Fixtures ---
27
-
28
-
29
- @pytest.fixture
30
- def mock_config_data() -> dict:
31
- """Mock config file data."""
32
- return {
33
- "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjk5OTk5OTk5OTl9.test",
34
- "refresh_token": "test-refresh-token",
35
- "base_url": "https://api.lyceum.dev",
36
- }
37
-
38
-
39
- @pytest.fixture
40
- def setup_config_file(tmp_path, mock_config_data):
41
- """Setup mock config file in tmp directory."""
42
-
43
- def _setup(config_data: dict = None):
44
- config_dir = tmp_path / ".lyceum"
45
- config_dir.mkdir(parents=True, exist_ok=True)
46
- config_file = config_dir / "config.json"
47
- config_file.write_text(json.dumps(config_data or mock_config_data))
48
- return config_file
49
-
50
- return _setup
51
-
52
-
53
- # --- HTTP Response Fixtures (following PR #502 pattern) ---
54
-
55
-
56
- @pytest.fixture
57
- def setup_httpx_response():
58
- """Setup mock for httpx response."""
59
-
60
- def _setup(status_code: int = 200, json_data: dict = None, text: str = "OK"):
61
- mock_response = MagicMock()
62
- mock_response.status_code = status_code
63
- mock_response.json.return_value = json_data or {}
64
- mock_response.text = text
65
- mock_response.content = text.encode()
66
- return mock_response
67
-
68
- return _setup
69
-
70
-
71
- @pytest.fixture
72
- def setup_httpx_post(setup_httpx_response):
73
- """Setup mock for httpx.post calls."""
74
-
75
- def _setup(status_code: int = 200, execution_id: str = "test-exec-id"):
76
- response = setup_httpx_response(
77
- status_code=status_code,
78
- json_data={
79
- "execution_id": execution_id,
80
- "streaming_url": f"https://api.lyceum.dev/stream/{execution_id}",
81
- "status": "queued",
82
- },
83
- )
84
- return response
85
-
86
- return _setup
87
-
88
-
89
- @pytest.fixture
90
- def setup_httpx_stream():
91
- """Setup mock for httpx.stream SSE responses."""
92
-
93
- def _setup(events: list[dict], status_code: int = 200):
94
- mock_response = MagicMock()
95
- mock_response.status_code = status_code
96
- mock_response.iter_lines.return_value = [
97
- f"data: {json.dumps(event)}" for event in events
98
- ]
99
-
100
- mock_context = MagicMock()
101
- mock_context.__enter__.return_value = mock_response
102
- mock_context.__exit__.return_value = None
103
-
104
- return mock_context
105
-
106
- return _setup
107
-
108
-
109
- # --- Machine/Quota Fixtures ---
110
-
111
-
112
- @pytest.fixture
113
- def setup_available_machines():
114
- """Setup mock for available machines API response."""
115
-
116
- def _setup(machines: list[str] = None):
117
- machines = machines or ["cpu", "a100", "h100"]
118
- mock_response = MagicMock()
119
- mock_response.status_code = 200
120
- mock_response.json.return_value = {
121
- "hardware_profiles": [{"hardware_profile": m} for m in machines]
122
- }
123
- return mock_response
124
-
125
- return _setup
126
-
127
-
128
- # --- File System Fixtures ---
129
-
130
-
131
- @pytest.fixture
132
- def sample_workspace(tmp_path):
133
- """Create a sample workspace with Python files and packages."""
134
- workspace = tmp_path / "workspace"
135
- workspace.mkdir()
136
-
137
- # Main file
138
- main_py = workspace / "main.py"
139
- main_py.write_text(
140
- '''"""Main script"""
141
- import numpy as np
142
- from mypackage import helper_function
143
- from standalone import standalone_func
144
-
145
- def main():
146
- print(helper_function())
147
- print(standalone_func())
148
-
149
- if __name__ == "__main__":
150
- main()
151
- '''
152
- )
153
-
154
- # Package
155
- pkg = workspace / "mypackage"
156
- pkg.mkdir()
157
- (pkg / "__init__.py").write_text(
158
- '''"""My package"""
159
- from .utils import helper_function
160
- '''
161
- )
162
- (pkg / "utils.py").write_text(
163
- '''"""Utils module"""
164
- def helper_function():
165
- return "Hello from utils"
166
- '''
167
- )
168
-
169
- # Standalone module
170
- (workspace / "standalone.py").write_text(
171
- '''"""Standalone module"""
172
- def standalone_func():
173
- return "Hello from standalone"
174
- '''
175
- )
176
-
177
- # Requirements file
178
- (workspace / "requirements.txt").write_text("numpy==1.24.0\n")
179
-
180
- return workspace
181
-
182
-
183
- @pytest.fixture
184
- def sample_workspace_config(sample_workspace):
185
- """Create .lyceum/config.json in sample workspace."""
186
-
187
- def _setup(config_data: dict = None):
188
- lyceum_dir = sample_workspace / ".lyceum"
189
- lyceum_dir.mkdir()
190
- config_file = lyceum_dir / "config.json"
191
-
192
- default_config = {
193
- "workspace": str(sample_workspace),
194
- "dependencies": {"merged": ["numpy==1.24.0"]},
195
- "local_packages": ["mypackage"],
196
- }
197
- config_file.write_text(json.dumps(config_data or default_config))
198
- return config_file
199
-
200
- return _setup
tests/unit/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Unit tests for Lyceum CLI."""
@@ -1 +0,0 @@
1
- """Unit tests for external modules."""
@@ -1 +0,0 @@
1
- """Unit tests for compute modules."""
@@ -1 +0,0 @@
1
- """Unit tests for execution modules."""
@@ -1,33 +0,0 @@
1
- """Shared test data for CLI tests."""
2
-
3
- VALID_EXECUTION_PAYLOAD = {
4
- "code": "print('hello')",
5
- "execution_type": "cpu",
6
- "timeout": 60,
7
- }
8
-
9
- VALID_EXECUTION_WITH_REQUIREMENTS = {
10
- **VALID_EXECUTION_PAYLOAD,
11
- "requirements_content": "numpy==1.24.0",
12
- }
13
-
14
- VALID_EXECUTION_WITH_IMPORTS = {
15
- **VALID_EXECUTION_PAYLOAD,
16
- "import_files": '{"mypackage/__init__.py": "# init", "mypackage/utils.py": "def foo(): pass"}',
17
- }
18
-
19
- SAMPLE_SSE_OUTPUT_EVENT = {"output": {"content": "Hello, World!\n"}}
20
-
21
- SAMPLE_SSE_JOB_FINISHED = {
22
- "jobFinished": {"job": {"result": {"returnCode": "0"}}}
23
- }
24
-
25
- SAMPLE_SSE_JOB_FAILED = {
26
- "jobFinished": {"job": {"result": {"returnCode": "1"}}}
27
- }
28
-
29
- SAMPLE_WORKSPACE_CONFIG = {
30
- "workspace": "/path/to/workspace",
31
- "dependencies": {"merged": ["numpy==1.24.0", "pandas>=2.0.0"]},
32
- "local_packages": ["mypackage"],
33
- }