podstack 1.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.
podstack_gpu/image.py ADDED
@@ -0,0 +1,325 @@
1
+ """Podstack Image - Fluent container image builder."""
2
+
3
+ from __future__ import annotations
4
+ from typing import List, Dict, Optional, Union
5
+ from dataclasses import dataclass, field
6
+ import json
7
+
8
+
9
+ @dataclass
10
+ class ImageDefinition:
11
+ """Serializable image definition."""
12
+ base: str = "python:3.11-slim"
13
+ python_version: str = "3.11"
14
+ pip_packages: List[str] = field(default_factory=list)
15
+ requirements_file: Optional[str] = None
16
+ apt_packages: List[str] = field(default_factory=list)
17
+ conda_packages: List[str] = field(default_factory=list)
18
+ env_vars: Dict[str, str] = field(default_factory=dict)
19
+ workdir: str = "/app"
20
+ run_commands: List[str] = field(default_factory=list)
21
+ copy_files: List[tuple] = field(default_factory=list)
22
+ env_preset: Optional[str] = None # ml, cv, nlp, audio, tabular, rl, scientific
23
+
24
+ def to_dict(self) -> dict:
25
+ """Convert to dictionary for API serialization."""
26
+ return {
27
+ "base": self.base,
28
+ "python_version": self.python_version,
29
+ "pip_packages": self.pip_packages,
30
+ "requirements_file": self.requirements_file,
31
+ "apt_packages": self.apt_packages,
32
+ "conda_packages": self.conda_packages,
33
+ "env_vars": self.env_vars,
34
+ "workdir": self.workdir,
35
+ "run_commands": self.run_commands,
36
+ "env_preset": self.env_preset,
37
+ }
38
+
39
+ def to_annotation(self) -> str:
40
+ """Convert to annotation string for legacy compatibility."""
41
+ parts = []
42
+
43
+ if self.env_preset:
44
+ parts.append(f"env={self.env_preset}")
45
+
46
+ if self.pip_packages:
47
+ parts.append(f"pip=[{','.join(self.pip_packages)}]")
48
+
49
+ if self.requirements_file:
50
+ parts.append(f"requirements={self.requirements_file}")
51
+
52
+ if self.conda_packages:
53
+ parts.append(f"conda=[{','.join(self.conda_packages)}]")
54
+
55
+ if self.python_version != "3.11":
56
+ parts.append(f"python={self.python_version}")
57
+
58
+ return " ".join(parts)
59
+
60
+
61
+ class Image:
62
+ """
63
+ Fluent container image builder for Podstack GPU.
64
+
65
+ Example:
66
+ image = (
67
+ podstack.Image.debian_slim()
68
+ .pip_install("torch", "transformers")
69
+ .pip_install_from_requirements("requirements.txt")
70
+ .apt_install("ffmpeg", "libsndfile1")
71
+ .env({"CUDA_VISIBLE_DEVICES": "0"})
72
+ )
73
+
74
+ # Full GPU for training
75
+ @app.function(gpu="H100", image=image)
76
+ def train():
77
+ import torch
78
+ ...
79
+
80
+ # Fractional GPU for inference (25%, 50%, 75%, or 100%)
81
+ @app.function(gpu="L40S", fraction=25, image=image)
82
+ def inference(data):
83
+ return model.predict(data)
84
+ """
85
+
86
+ def __init__(self, definition: Optional[ImageDefinition] = None):
87
+ self._definition = definition or ImageDefinition()
88
+
89
+ def _copy(self) -> Image:
90
+ """Create a copy of this image for chaining."""
91
+ import copy
92
+ return Image(copy.deepcopy(self._definition))
93
+
94
+ # ===== Base Image Factory Methods =====
95
+
96
+ @classmethod
97
+ def debian_slim(cls, python_version: str = "3.11") -> Image:
98
+ """Create image based on Debian slim with Python."""
99
+ return cls(ImageDefinition(
100
+ base=f"python:{python_version}-slim",
101
+ python_version=python_version,
102
+ ))
103
+
104
+ @classmethod
105
+ def from_registry(cls, image_name: str) -> Image:
106
+ """Create image from a Docker registry."""
107
+ return cls(ImageDefinition(base=image_name))
108
+
109
+ @classmethod
110
+ def from_dockerfile(cls, path: str, context_dir: str = ".") -> Image:
111
+ """Create image from a Dockerfile."""
112
+ img = cls(ImageDefinition())
113
+ img._definition.base = f"dockerfile:{path}"
114
+ return img
115
+
116
+ @classmethod
117
+ def micromamba(cls, python_version: str = "3.11") -> Image:
118
+ """Create image with micromamba for conda packages."""
119
+ return cls(ImageDefinition(
120
+ base="mambaorg/micromamba:latest",
121
+ python_version=python_version,
122
+ ))
123
+
124
+ # ===== Environment Preset Factory Methods =====
125
+
126
+ @classmethod
127
+ def ml(cls) -> Image:
128
+ """Pre-configured image for machine learning (PyTorch, scikit-learn, etc.)."""
129
+ img = cls.debian_slim()
130
+ img._definition.env_preset = "ml"
131
+ return img
132
+
133
+ @classmethod
134
+ def cv(cls) -> Image:
135
+ """Pre-configured image for computer vision (PyTorch, torchvision, opencv, etc.)."""
136
+ img = cls.debian_slim()
137
+ img._definition.env_preset = "cv"
138
+ return img
139
+
140
+ @classmethod
141
+ def nlp(cls) -> Image:
142
+ """Pre-configured image for NLP (transformers, tokenizers, etc.)."""
143
+ img = cls.debian_slim()
144
+ img._definition.env_preset = "nlp"
145
+ return img
146
+
147
+ @classmethod
148
+ def audio(cls) -> Image:
149
+ """Pre-configured image for audio processing (torchaudio, librosa, etc.)."""
150
+ img = cls.debian_slim()
151
+ img._definition.env_preset = "audio"
152
+ return img
153
+
154
+ @classmethod
155
+ def tabular(cls) -> Image:
156
+ """Pre-configured image for tabular data (pandas, xgboost, etc.)."""
157
+ img = cls.debian_slim()
158
+ img._definition.env_preset = "tabular"
159
+ return img
160
+
161
+ @classmethod
162
+ def rl(cls) -> Image:
163
+ """Pre-configured image for reinforcement learning (gymnasium, stable-baselines3, etc.)."""
164
+ img = cls.debian_slim()
165
+ img._definition.env_preset = "rl"
166
+ return img
167
+
168
+ @classmethod
169
+ def scientific(cls) -> Image:
170
+ """Pre-configured image for scientific computing (numpy, scipy, matplotlib, etc.)."""
171
+ img = cls.debian_slim()
172
+ img._definition.env_preset = "scientific"
173
+ return img
174
+
175
+ # ===== Fluent Builder Methods =====
176
+
177
+ def pip_install(self, *packages: str, find_links: str = None,
178
+ index_url: str = None, extra_index_url: str = None) -> Image:
179
+ """
180
+ Install pip packages.
181
+
182
+ Args:
183
+ *packages: Package names (e.g., "torch", "transformers>=4.30.0")
184
+ find_links: URL to find packages
185
+ index_url: Custom PyPI index URL
186
+ extra_index_url: Additional PyPI index URL
187
+
188
+ Example:
189
+ image.pip_install("torch", "transformers>=4.30.0")
190
+ image.pip_install("torch", index_url="https://download.pytorch.org/whl/cu118")
191
+ """
192
+ new_img = self._copy()
193
+ new_img._definition.pip_packages.extend(packages)
194
+ return new_img
195
+
196
+ def pip_install_from_requirements(self, path: str) -> Image:
197
+ """
198
+ Install packages from a requirements.txt file.
199
+
200
+ Args:
201
+ path: Path to requirements.txt file
202
+
203
+ Example:
204
+ image.pip_install_from_requirements("requirements.txt")
205
+ """
206
+ new_img = self._copy()
207
+ new_img._definition.requirements_file = path
208
+ return new_img
209
+
210
+ def apt_install(self, *packages: str) -> Image:
211
+ """
212
+ Install apt packages (Debian/Ubuntu).
213
+
214
+ Args:
215
+ *packages: Package names (e.g., "ffmpeg", "libsndfile1")
216
+
217
+ Example:
218
+ image.apt_install("ffmpeg", "libsndfile1")
219
+ """
220
+ new_img = self._copy()
221
+ new_img._definition.apt_packages.extend(packages)
222
+ return new_img
223
+
224
+ def conda_install(self, *packages: str, channels: List[str] = None) -> Image:
225
+ """
226
+ Install conda packages.
227
+
228
+ Args:
229
+ *packages: Package names
230
+ channels: Conda channels to use
231
+
232
+ Example:
233
+ image.conda_install("pytorch", "cudatoolkit=11.8", channels=["pytorch"])
234
+ """
235
+ new_img = self._copy()
236
+ new_img._definition.conda_packages.extend(packages)
237
+ return new_img
238
+
239
+ def env(self, env_vars: Dict[str, str]) -> Image:
240
+ """
241
+ Set environment variables.
242
+
243
+ Args:
244
+ env_vars: Dictionary of environment variables
245
+
246
+ Example:
247
+ image.env({"CUDA_VISIBLE_DEVICES": "0", "HF_HOME": "/cache/huggingface"})
248
+ """
249
+ new_img = self._copy()
250
+ new_img._definition.env_vars.update(env_vars)
251
+ return new_img
252
+
253
+ def workdir(self, path: str) -> Image:
254
+ """
255
+ Set the working directory.
256
+
257
+ Args:
258
+ path: Working directory path
259
+
260
+ Example:
261
+ image.workdir("/app/src")
262
+ """
263
+ new_img = self._copy()
264
+ new_img._definition.workdir = path
265
+ return new_img
266
+
267
+ def run_commands(self, *commands: str) -> Image:
268
+ """
269
+ Run shell commands during image build.
270
+
271
+ Args:
272
+ *commands: Shell commands to run
273
+
274
+ Example:
275
+ image.run_commands("apt-get update", "apt-get install -y curl")
276
+ """
277
+ new_img = self._copy()
278
+ new_img._definition.run_commands.extend(commands)
279
+ return new_img
280
+
281
+ def copy_local_file(self, local_path: str, remote_path: str) -> Image:
282
+ """
283
+ Copy a local file into the image.
284
+
285
+ Args:
286
+ local_path: Local file path
287
+ remote_path: Path in the container
288
+
289
+ Example:
290
+ image.copy_local_file("./model.py", "/app/model.py")
291
+ """
292
+ new_img = self._copy()
293
+ new_img._definition.copy_files.append((local_path, remote_path))
294
+ return new_img
295
+
296
+ def add_local_dir(self, local_path: str, remote_path: str) -> Image:
297
+ """
298
+ Add a local directory to the image.
299
+
300
+ Args:
301
+ local_path: Local directory path
302
+ remote_path: Path in the container
303
+ """
304
+ return self.copy_local_file(local_path, remote_path)
305
+
306
+ # ===== Properties and Serialization =====
307
+
308
+ @property
309
+ def definition(self) -> ImageDefinition:
310
+ """Get the image definition."""
311
+ return self._definition
312
+
313
+ def to_dict(self) -> dict:
314
+ """Convert to dictionary for API serialization."""
315
+ return self._definition.to_dict()
316
+
317
+ def __repr__(self) -> str:
318
+ parts = [f"Image(base={self._definition.base!r}"]
319
+ if self._definition.env_preset:
320
+ parts.append(f", env={self._definition.env_preset!r}")
321
+ if self._definition.pip_packages:
322
+ parts.append(f", pip={self._definition.pip_packages}")
323
+ if self._definition.apt_packages:
324
+ parts.append(f", apt={self._definition.apt_packages}")
325
+ return "".join(parts) + ")"