modal-uv 0.2.0__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.
modal_uv/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Run uv commands on Modal.com with GPU and persistent volumes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.2.0"
modal_uv/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Allow running as `python -m modal_uv`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from modal_uv.cli import app
6
+
7
+ app()
modal_uv/app.py ADDED
@@ -0,0 +1,147 @@
1
+ """Modal app definition for modal-uv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import threading
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import modal
12
+
13
+ from modal_uv.sync import (
14
+ STATE_FILE_NAME,
15
+ FilePayload,
16
+ FileState,
17
+ plan_sync,
18
+ save_state_csv,
19
+ uv_run_command,
20
+ uv_run_env,
21
+ )
22
+
23
+ _INFRA_ENV = {
24
+ "PATH": "/root/.local/bin:/usr/local/bin:/usr/bin:/bin",
25
+ "UV_LINK_MODE": "copy",
26
+ "UV_PROJECT_ENVIRONMENT": "/usr/local",
27
+ }
28
+
29
+
30
+ def create_app(
31
+ app_name: str,
32
+ gpu: str | None,
33
+ volumes: list[dict[str, Any]],
34
+ env: dict[str, str],
35
+ scaledown_window_seconds: int,
36
+ runtime_exec: str | None,
37
+ work_dir: str,
38
+ image_base: str,
39
+ fingerprint: str,
40
+ ) -> modal.App:
41
+ """Create a Modal app from config."""
42
+ app = modal.App(name=app_name)
43
+
44
+ modal_volumes: dict[str, modal.Volume] = {}
45
+ for vol in volumes:
46
+ mv = modal.Volume.from_name(vol["name"], create_if_missing=True)
47
+ modal_volumes[vol["mount_path"]] = mv
48
+
49
+ merged_env = {**_INFRA_ENV, **env}
50
+
51
+ image = (
52
+ modal.Image.from_registry(image_base)
53
+ .apt_install("curl")
54
+ .run_commands("curl -LsSf https://astral.sh/uv/install.sh | sh")
55
+ .pip_install("pathspec")
56
+ .env(merged_env)
57
+ .add_local_python_source("modal_uv")
58
+ )
59
+
60
+ @app.function(image=image, serialized=True, name="deployment_fingerprint")
61
+ def _deployment_fingerprint() -> str:
62
+ return fingerprint
63
+
64
+ commit_specs: list[tuple[modal.Volume, int]] = [
65
+ (mv, vol["commit_interval_seconds"])
66
+ for vol, mv in zip(volumes, modal_volumes.values(), strict=True)
67
+ ]
68
+
69
+ cls_options: dict[str, Any] = {
70
+ "scaledown_window": scaledown_window_seconds,
71
+ "timeout": 7200,
72
+ "max_containers": 1,
73
+ "image": image,
74
+ "serialized": True,
75
+ }
76
+ if modal_volumes:
77
+ cls_options["volumes"] = modal_volumes
78
+ if gpu is not None:
79
+ cls_options["gpu"] = gpu
80
+
81
+ @app.cls(**cls_options)
82
+ @modal.concurrent(max_inputs=1)
83
+ class Worker:
84
+ """Sync files and execute commands on Modal."""
85
+
86
+ @modal.method()
87
+ def plan_sync(self, manifest: list[FileState]) -> list[str]:
88
+ """Delete extra remote files and return missing/stale paths."""
89
+ return plan_sync(Path(work_dir), manifest)
90
+
91
+ @modal.method()
92
+ def sync_and_run(
93
+ self,
94
+ manifest: list[FileState],
95
+ files: list[FilePayload],
96
+ args: list[str],
97
+ mode: str = "run",
98
+ ) -> int:
99
+ """Upload missing files and execute a command."""
100
+ os.makedirs(work_dir, exist_ok=True)
101
+ for file in files:
102
+ file.write_to(Path(work_dir))
103
+ save_state_csv(Path(work_dir) / STATE_FILE_NAME, manifest)
104
+
105
+ if mode == "run":
106
+ command = uv_run_command(args)
107
+ elif mode == "exec":
108
+ if not args or not args[0].strip():
109
+ raise ValueError("exec command is required")
110
+ shell = runtime_exec or os.environ.get("SHELL") or "/bin/sh"
111
+ command = [shell, "-c", args[0]]
112
+ else:
113
+ raise ValueError(f"unknown execution mode: {mode}")
114
+
115
+ stop_event = threading.Event()
116
+ threads: list[threading.Thread] = []
117
+
118
+ for vol_obj, interval in commit_specs:
119
+
120
+ def make_commit_thread(v: modal.Volume, iv: int) -> threading.Thread:
121
+ def loop() -> None:
122
+ while not stop_event.wait(iv):
123
+ try:
124
+ v.commit()
125
+ except Exception as exc:
126
+ print(f"[modal-uv] periodic commit failed: {exc}", flush=True)
127
+
128
+ return threading.Thread(target=loop, daemon=True)
129
+
130
+ t = make_commit_thread(vol_obj, interval)
131
+ t.start()
132
+ threads.append(t)
133
+
134
+ result = subprocess.run(
135
+ command,
136
+ cwd=work_dir,
137
+ env=uv_run_env(Path(work_dir)),
138
+ )
139
+ stop_event.set()
140
+ for t in threads:
141
+ t.join(timeout=10)
142
+
143
+ for vol_obj, _ in commit_specs:
144
+ vol_obj.commit()
145
+ return result.returncode
146
+
147
+ return app