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/secret.py ADDED
@@ -0,0 +1,189 @@
1
+ """Podstack Secret - Secure credential management for GPU functions."""
2
+
3
+ from __future__ import annotations
4
+ import os
5
+ from typing import Optional, Dict
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class SecretConfig:
11
+ """Secret configuration."""
12
+ name: str
13
+ env_var: Optional[str] = None # Environment variable to inject as
14
+
15
+ def to_dict(self) -> dict:
16
+ return {
17
+ "name": self.name,
18
+ "env_var": self.env_var,
19
+ }
20
+
21
+
22
+ class Secret:
23
+ """
24
+ Secure secrets that can be injected into GPU functions.
25
+
26
+ Secrets are stored encrypted and only decrypted at runtime.
27
+ They can be injected as environment variables or accessed via the API.
28
+
29
+ Example:
30
+ # Reference a secret stored in Podstack
31
+ hf_token = podstack.Secret.from_name("huggingface-token")
32
+
33
+ @app.function(gpu="H100", secrets=[hf_token])
34
+ def train():
35
+ import os
36
+ # Secret is available as environment variable
37
+ token = os.environ["HUGGINGFACE_TOKEN"]
38
+
39
+ # Or inject from environment at deploy time
40
+ api_key = podstack.Secret.from_local_env("OPENAI_API_KEY")
41
+ """
42
+
43
+ def __init__(self, config: SecretConfig):
44
+ self._config = config
45
+
46
+ @classmethod
47
+ def from_name(
48
+ cls,
49
+ name: str,
50
+ environment_variable: str = None,
51
+ required: bool = True,
52
+ ) -> "Secret":
53
+ """
54
+ Reference a secret stored in Podstack by name.
55
+
56
+ Args:
57
+ name: Secret name in Podstack
58
+ environment_variable: Env var name to inject as (defaults to uppercased name)
59
+ required: If True, fail if secret doesn't exist
60
+
61
+ Example:
62
+ secret = podstack.Secret.from_name(
63
+ "huggingface-token",
64
+ environment_variable="HF_TOKEN"
65
+ )
66
+ """
67
+ env_var = environment_variable or name.upper().replace("-", "_")
68
+ return cls(SecretConfig(name=name, env_var=env_var))
69
+
70
+ @classmethod
71
+ def from_local_env(
72
+ cls,
73
+ env_var: str,
74
+ remote_name: str = None,
75
+ ) -> "Secret":
76
+ """
77
+ Create a secret from a local environment variable.
78
+
79
+ The value is read at deploy time and stored securely.
80
+
81
+ Args:
82
+ env_var: Local environment variable name
83
+ remote_name: Name to use remotely (defaults to env_var)
84
+
85
+ Example:
86
+ # Read OPENAI_API_KEY from local env and inject into function
87
+ secret = podstack.Secret.from_local_env("OPENAI_API_KEY")
88
+ """
89
+ return cls(SecretConfig(
90
+ name=remote_name or env_var,
91
+ env_var=env_var,
92
+ ))
93
+
94
+ @classmethod
95
+ def from_dict(cls, secrets: Dict[str, str]) -> "Secret":
96
+ """
97
+ Create a secret from a dictionary (for testing/development).
98
+
99
+ WARNING: Do not use in production! Values will be in code.
100
+
101
+ Args:
102
+ secrets: Dictionary of secret key-value pairs
103
+
104
+ Example:
105
+ # Only for local testing!
106
+ secret = podstack.Secret.from_dict({"API_KEY": "test-key"})
107
+ """
108
+ # Create a composite secret
109
+ return cls(SecretConfig(name="_dict_secret"))
110
+
111
+ @classmethod
112
+ def from_dotenv(cls, path: str = ".env") -> "Secret":
113
+ """
114
+ Load secrets from a .env file.
115
+
116
+ Args:
117
+ path: Path to .env file
118
+
119
+ Example:
120
+ secrets = podstack.Secret.from_dotenv(".env.production")
121
+ """
122
+ return cls(SecretConfig(name=f"_dotenv:{path}"))
123
+
124
+ @property
125
+ def name(self) -> str:
126
+ """Get the secret name."""
127
+ return self._config.name
128
+
129
+ @property
130
+ def env_var(self) -> str:
131
+ """Get the environment variable name."""
132
+ return self._config.env_var
133
+
134
+ def to_dict(self) -> dict:
135
+ """Convert to dictionary for API serialization."""
136
+ return self._config.to_dict()
137
+
138
+ def __repr__(self) -> str:
139
+ return f"Secret(name={self._config.name!r})"
140
+
141
+
142
+ class SecretDict:
143
+ """
144
+ A dictionary of secrets that can be accessed in GPU functions.
145
+
146
+ Example:
147
+ secrets = podstack.SecretDict.from_name("my-secrets")
148
+
149
+ @app.function(gpu="H100", secrets=[secrets])
150
+ def train():
151
+ # Access secrets by key
152
+ api_key = secrets["API_KEY"]
153
+ """
154
+
155
+ def __init__(self, name: str):
156
+ self._name = name
157
+ self._values: Dict[str, str] = {}
158
+
159
+ @classmethod
160
+ def from_name(cls, name: str) -> "SecretDict":
161
+ """
162
+ Reference a secret dictionary stored in Podstack.
163
+
164
+ Args:
165
+ name: Secret dictionary name
166
+ """
167
+ return cls(name)
168
+
169
+ def __getitem__(self, key: str) -> str:
170
+ """Get a secret value by key."""
171
+ if key in self._values:
172
+ return self._values[key]
173
+ # In remote execution, this would be populated
174
+ return os.environ.get(f"{self._name.upper()}_{key.upper()}", "")
175
+
176
+ def get(self, key: str, default: str = None) -> Optional[str]:
177
+ """Get a secret value with a default."""
178
+ try:
179
+ return self[key]
180
+ except KeyError:
181
+ return default
182
+
183
+ @property
184
+ def name(self) -> str:
185
+ """Get the secret dict name."""
186
+ return self._name
187
+
188
+ def __repr__(self) -> str:
189
+ return f"SecretDict(name={self._name!r})"
podstack_gpu/utils.py ADDED
@@ -0,0 +1,203 @@
1
+ """Podstack GPU Utilities - Helper functions for GPU operations."""
2
+
3
+ import subprocess
4
+ import os
5
+ from typing import Optional, Dict, Any, List
6
+
7
+
8
+ def nvidia_smi(format: str = "text") -> str:
9
+ """
10
+ Get nvidia-smi output.
11
+
12
+ This function runs nvidia-smi and returns the output.
13
+ Use this inside your GPU functions to check GPU status.
14
+
15
+ Args:
16
+ format: Output format - "text" (default), "csv", or "xml"
17
+
18
+ Returns:
19
+ nvidia-smi output as string
20
+
21
+ Example:
22
+ @app.function(gpu="L40S")
23
+ def check_gpu():
24
+ from podstack import nvidia_smi
25
+ print(nvidia_smi())
26
+ return {"gpu_info": nvidia_smi("csv")}
27
+ """
28
+ try:
29
+ if format == "csv":
30
+ cmd = ["nvidia-smi", "--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu", "--format=csv"]
31
+ elif format == "xml":
32
+ cmd = ["nvidia-smi", "-x", "-q"]
33
+ else:
34
+ cmd = ["nvidia-smi"]
35
+
36
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
37
+ return result.stdout if result.returncode == 0 else result.stderr
38
+ except FileNotFoundError:
39
+ return "nvidia-smi not found. Make sure NVIDIA drivers are installed."
40
+ except subprocess.TimeoutExpired:
41
+ return "nvidia-smi timed out"
42
+ except Exception as e:
43
+ return f"Error running nvidia-smi: {e}"
44
+
45
+
46
+ def gpu_info() -> Dict[str, Any]:
47
+ """
48
+ Get GPU information as a dictionary.
49
+
50
+ Returns:
51
+ Dictionary with GPU information including:
52
+ - name: GPU model name
53
+ - memory_total: Total memory in MB
54
+ - memory_used: Used memory in MB
55
+ - memory_free: Free memory in MB
56
+ - utilization: GPU utilization percentage
57
+ - temperature: GPU temperature in Celsius
58
+ - count: Number of GPUs
59
+
60
+ Example:
61
+ @app.function(gpu="L40S")
62
+ def check_resources():
63
+ from podstack import gpu_info
64
+ info = gpu_info()
65
+ print(f"GPU: {info['name']}")
66
+ print(f"Memory: {info['memory_used']}/{info['memory_total']} MB")
67
+ return info
68
+ """
69
+ try:
70
+ result = subprocess.run(
71
+ ["nvidia-smi", "--query-gpu=name,memory.total,memory.used,memory.free,utilization.gpu,temperature.gpu,count", "--format=csv,noheader,nounits"],
72
+ capture_output=True, text=True, timeout=10
73
+ )
74
+
75
+ if result.returncode != 0:
76
+ return {"error": result.stderr}
77
+
78
+ lines = result.stdout.strip().split("\n")
79
+ gpus = []
80
+
81
+ for line in lines:
82
+ parts = [p.strip() for p in line.split(",")]
83
+ if len(parts) >= 6:
84
+ gpus.append({
85
+ "name": parts[0],
86
+ "memory_total": int(parts[1]) if parts[1].isdigit() else parts[1],
87
+ "memory_used": int(parts[2]) if parts[2].isdigit() else parts[2],
88
+ "memory_free": int(parts[3]) if parts[3].isdigit() else parts[3],
89
+ "utilization": int(parts[4]) if parts[4].isdigit() else parts[4],
90
+ "temperature": int(parts[5]) if parts[5].isdigit() else parts[5],
91
+ })
92
+
93
+ if len(gpus) == 1:
94
+ return gpus[0]
95
+
96
+ return {
97
+ "count": len(gpus),
98
+ "gpus": gpus,
99
+ "name": gpus[0]["name"] if gpus else "Unknown",
100
+ "memory_total": sum(g.get("memory_total", 0) for g in gpus if isinstance(g.get("memory_total"), int)),
101
+ "memory_used": sum(g.get("memory_used", 0) for g in gpus if isinstance(g.get("memory_used"), int)),
102
+ "memory_free": sum(g.get("memory_free", 0) for g in gpus if isinstance(g.get("memory_free"), int)),
103
+ }
104
+
105
+ except FileNotFoundError:
106
+ return {"error": "nvidia-smi not found"}
107
+ except Exception as e:
108
+ return {"error": str(e)}
109
+
110
+
111
+ def cuda_available() -> bool:
112
+ """
113
+ Check if CUDA is available.
114
+
115
+ Returns:
116
+ True if CUDA is available, False otherwise
117
+
118
+ Example:
119
+ @app.function(gpu="L40S")
120
+ def train():
121
+ from podstack import cuda_available
122
+ if cuda_available():
123
+ print("CUDA is ready!")
124
+ else:
125
+ print("Warning: CUDA not available")
126
+ """
127
+ try:
128
+ import torch
129
+ return torch.cuda.is_available()
130
+ except ImportError:
131
+ # Fallback to checking nvidia-smi
132
+ try:
133
+ result = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5)
134
+ return result.returncode == 0
135
+ except:
136
+ return False
137
+
138
+
139
+ def cuda_device_count() -> int:
140
+ """
141
+ Get the number of available CUDA devices.
142
+
143
+ Returns:
144
+ Number of CUDA devices
145
+
146
+ Example:
147
+ @app.function(gpu="A100-80G", count=4)
148
+ def distributed_train():
149
+ from podstack import cuda_device_count
150
+ print(f"Training on {cuda_device_count()} GPUs")
151
+ """
152
+ try:
153
+ import torch
154
+ return torch.cuda.device_count()
155
+ except ImportError:
156
+ # Fallback
157
+ info = gpu_info()
158
+ return info.get("count", 1) if "error" not in info else 0
159
+
160
+
161
+ def print_gpu_status():
162
+ """
163
+ Print a formatted GPU status summary.
164
+
165
+ Example:
166
+ @app.function(gpu="L40S")
167
+ def my_function():
168
+ from podstack import print_gpu_status
169
+ print_gpu_status()
170
+ # ... your code ...
171
+ """
172
+ print("=" * 60)
173
+ print("GPU STATUS")
174
+ print("=" * 60)
175
+
176
+ info = gpu_info()
177
+
178
+ if "error" in info:
179
+ print(f"Error: {info['error']}")
180
+ return
181
+
182
+ if "gpus" in info:
183
+ # Multiple GPUs
184
+ print(f"Total GPUs: {info['count']}")
185
+ print(f"Total Memory: {info['memory_used']:,} / {info['memory_total']:,} MB")
186
+ print("-" * 60)
187
+ for i, gpu in enumerate(info["gpus"]):
188
+ print(f"GPU {i}: {gpu['name']}")
189
+ print(f" Memory: {gpu['memory_used']:,} / {gpu['memory_total']:,} MB ({gpu['memory_free']:,} MB free)")
190
+ print(f" Utilization: {gpu['utilization']}%")
191
+ print(f" Temperature: {gpu['temperature']}°C")
192
+ else:
193
+ # Single GPU
194
+ print(f"GPU: {info.get('name', 'Unknown')}")
195
+ mem_used = info.get('memory_used', 0)
196
+ mem_total = info.get('memory_total', 0)
197
+ mem_free = info.get('memory_free', 0)
198
+ if isinstance(mem_used, int) and isinstance(mem_total, int):
199
+ print(f"Memory: {mem_used:,} / {mem_total:,} MB ({mem_free:,} MB free)")
200
+ print(f"Utilization: {info.get('utilization', 'N/A')}%")
201
+ print(f"Temperature: {info.get('temperature', 'N/A')}°C")
202
+
203
+ print("=" * 60)
podstack_gpu/volume.py ADDED
@@ -0,0 +1,198 @@
1
+ """Podstack Volume - Persistent storage for GPU functions."""
2
+
3
+ from __future__ import annotations
4
+ import os
5
+ from typing import Optional
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass
10
+ class VolumeConfig:
11
+ """Volume configuration."""
12
+ name: str
13
+ size_gb: int = 10
14
+ region: Optional[str] = None
15
+
16
+ def to_dict(self) -> dict:
17
+ return {
18
+ "name": self.name,
19
+ "size_gb": self.size_gb,
20
+ "region": self.region,
21
+ }
22
+
23
+
24
+ class Volume:
25
+ """
26
+ Persistent network storage that can be attached to GPU functions.
27
+
28
+ Volumes persist data across function invocations and can be shared
29
+ between functions.
30
+
31
+ Example:
32
+ # Create a volume
33
+ model_volume = podstack.Volume.from_name("model-cache", create_if_missing=True)
34
+
35
+ @app.function(gpu="H100", volumes={"/models": model_volume})
36
+ def train():
37
+ # Save models to /models - they persist!
38
+ torch.save(model, "/models/checkpoint.pt")
39
+
40
+ @app.function(gpu="H100", volumes={"/models": model_volume})
41
+ def inference():
42
+ # Load the model saved during training
43
+ model = torch.load("/models/checkpoint.pt")
44
+ """
45
+
46
+ def __init__(self, config: VolumeConfig):
47
+ self._config = config
48
+ self._persisted = False
49
+
50
+ @classmethod
51
+ def from_name(
52
+ cls,
53
+ name: str,
54
+ create_if_missing: bool = True,
55
+ size_gb: int = 10,
56
+ region: str = None,
57
+ ) -> "Volume":
58
+ """
59
+ Get or create a volume by name.
60
+
61
+ Args:
62
+ name: Volume name (must be unique within project)
63
+ create_if_missing: Create the volume if it doesn't exist
64
+ size_gb: Size in GB (only used when creating)
65
+ region: Region for the volume (optional)
66
+
67
+ Example:
68
+ volume = podstack.Volume.from_name("my-data", size_gb=100)
69
+ """
70
+ return cls(VolumeConfig(
71
+ name=name,
72
+ size_gb=size_gb,
73
+ region=region,
74
+ ))
75
+
76
+ @classmethod
77
+ def ephemeral(cls, size_gb: int = 10) -> "Volume":
78
+ """
79
+ Create an ephemeral volume that only lasts for the function invocation.
80
+
81
+ Useful for temporary storage during execution.
82
+
83
+ Args:
84
+ size_gb: Size in GB
85
+
86
+ Example:
87
+ temp = podstack.Volume.ephemeral(size_gb=50)
88
+
89
+ @app.function(gpu="H100", volumes={"/scratch": temp})
90
+ def process():
91
+ # Use /scratch for temporary files
92
+ ...
93
+ """
94
+ import uuid
95
+ return cls(VolumeConfig(
96
+ name=f"ephemeral-{uuid.uuid4().hex[:8]}",
97
+ size_gb=size_gb,
98
+ ))
99
+
100
+ @property
101
+ def name(self) -> str:
102
+ """Get the volume name."""
103
+ return self._config.name
104
+
105
+ @property
106
+ def size_gb(self) -> int:
107
+ """Get the volume size in GB."""
108
+ return self._config.size_gb
109
+
110
+ def to_dict(self) -> dict:
111
+ """Convert to dictionary for API serialization."""
112
+ return self._config.to_dict()
113
+
114
+ def __repr__(self) -> str:
115
+ return f"Volume(name={self._config.name!r}, size_gb={self._config.size_gb})"
116
+
117
+
118
+ class CloudBucketMount:
119
+ """
120
+ Mount a cloud storage bucket (S3, GCS, etc.) as a volume.
121
+
122
+ Example:
123
+ bucket = podstack.CloudBucketMount.from_s3(
124
+ bucket_name="my-training-data",
125
+ secret=podstack.Secret.from_name("aws-credentials"),
126
+ )
127
+
128
+ @app.function(gpu="H100", volumes={"/data": bucket})
129
+ def train():
130
+ # Access S3 data at /data
131
+ data = load_data("/data/dataset.parquet")
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ bucket_name: str,
137
+ provider: str,
138
+ secret: "Secret" = None,
139
+ read_only: bool = False,
140
+ prefix: str = None,
141
+ ):
142
+ self._bucket_name = bucket_name
143
+ self._provider = provider
144
+ self._secret = secret
145
+ self._read_only = read_only
146
+ self._prefix = prefix
147
+
148
+ @classmethod
149
+ def from_s3(
150
+ cls,
151
+ bucket_name: str,
152
+ secret: "Secret" = None,
153
+ read_only: bool = False,
154
+ prefix: str = None,
155
+ ) -> "CloudBucketMount":
156
+ """
157
+ Mount an S3 bucket.
158
+
159
+ Args:
160
+ bucket_name: S3 bucket name
161
+ secret: Secret containing AWS credentials
162
+ read_only: Mount as read-only
163
+ prefix: Only mount objects with this prefix
164
+ """
165
+ return cls(bucket_name, "s3", secret, read_only, prefix)
166
+
167
+ @classmethod
168
+ def from_gcs(
169
+ cls,
170
+ bucket_name: str,
171
+ secret: "Secret" = None,
172
+ read_only: bool = False,
173
+ prefix: str = None,
174
+ ) -> "CloudBucketMount":
175
+ """
176
+ Mount a Google Cloud Storage bucket.
177
+
178
+ Args:
179
+ bucket_name: GCS bucket name
180
+ secret: Secret containing GCP credentials
181
+ read_only: Mount as read-only
182
+ prefix: Only mount objects with this prefix
183
+ """
184
+ return cls(bucket_name, "gcs", secret, read_only, prefix)
185
+
186
+ def to_dict(self) -> dict:
187
+ """Convert to dictionary for API serialization."""
188
+ return {
189
+ "type": "cloud_bucket",
190
+ "bucket_name": self._bucket_name,
191
+ "provider": self._provider,
192
+ "secret": self._secret.name if self._secret else None,
193
+ "read_only": self._read_only,
194
+ "prefix": self._prefix,
195
+ }
196
+
197
+ def __repr__(self) -> str:
198
+ return f"CloudBucketMount({self._provider}://{self._bucket_name})"