geronimo.artifacts
Geronimo Artifact Store.
The artifacts module provides a unified interface for storing and retrieving machine learning artifacts such as trained models, encoders, and other binary blobs. It supports versioned storage to ensure reproducibility and traceability.
Key components:
- ArtifactStore: The main entry point for saving and loading artifacts.
- ArtifactBackend: Protocol that defines the storage backend interface.
Supported backends:
- LocalArtifactBackend: Stores artifacts on the local filesystem.
- S3ArtifactBackend: Stores artifacts in an AWS S3 bucket.
- GeronimoDeployCloudArtifactBackend: Managed artifact storage via Geronimo Deploy Cloud.
- MLflowArtifactStore: Optional integration for tracking artifacts with MLflow.
1"""Geronimo Artifact Store. 2 3The artifacts module provides a unified interface for storing and retrieving 4machine learning artifacts such as trained models, encoders, and other binary blobs. 5It supports versioned storage to ensure reproducibility and traceability. 6 7Key components: 8- ArtifactStore: The main entry point for saving and loading artifacts. 9- ArtifactBackend: Protocol that defines the storage backend interface. 10 11Supported backends: 12- LocalArtifactBackend: Stores artifacts on the local filesystem. 13- S3ArtifactBackend: Stores artifacts in an AWS S3 bucket. 14- GeronimoDeployCloudArtifactBackend: Managed artifact storage via Geronimo Deploy Cloud. 15- MLflowArtifactStore: Optional integration for tracking artifacts with MLflow. 16""" 17 18from geronimo.artifacts.store import ArtifactStore 19from geronimo.artifacts.protocol import ArtifactBackend 20from geronimo.artifacts.local_backend import LocalArtifactBackend 21from geronimo.artifacts.s3_backend import S3ArtifactBackend 22from geronimo.artifacts.gdc_backend import GeronimoDeployCloudArtifactBackend 23 24# Optional MLflow backend 25try: 26 from geronimo.artifacts.mlflow_backend import MLflowArtifactStore 27except ImportError: 28 MLflowArtifactStore = None 29 30__all__ = [ 31 "ArtifactStore", 32 "ArtifactBackend", 33 "LocalArtifactBackend", 34 "S3ArtifactBackend", 35 "GeronimoDeployCloudArtifactBackend", 36 "MLflowArtifactStore", 37] 38 39 40__docformat__ = "google"
25class ArtifactStore: 26 """Versioned storage for ML artifacts. 27 28 Supports local filesystem, S3, and Geronimo Cloud backends. 29 Reads defaults from ~/.geronimo/config.yaml (run `geronimo config show`). 30 31 Example: 32 ```python 33 from geronimo.artifacts import ArtifactStore 34 35 # Save during training (uses global config defaults) 36 store = ArtifactStore(project="credit-risk", version="1.2.0") 37 store.save("model", trained_model) 38 store.save("encoder", fitted_encoder) 39 40 # Load in production 41 store = ArtifactStore.load(project="credit-risk", version="1.2.0") 42 model = store.get("model") 43 encoder = store.get("encoder") 44 ``` 45 46 Configuration: 47 Run `geronimo config init` to set up your preferred backend. 48 Run `geronimo config show` to view current settings. 49 """ 50 51 project: str 52 """The project name.""" 53 54 version: str 55 """The version string (e.g., "1.2.0").""" 56 57 backend: str 58 """The active storage backend ("local", "s3", "gdc", "mlflow", or "custom").""" 59 60 base_path: Optional[str] 61 """Base path for local storage (if using local backend).""" 62 63 s3_bucket: Optional[str] 64 """S3 bucket name (if using s3 backend).""" 65 66 def __init__( 67 self, 68 project: str, 69 version: str, 70 backend: Optional[Union[Literal["local", "s3", "gdc", "mlflow"], ArtifactBackend]] = None, 71 base_path: Optional[str] = None, 72 s3_bucket: Optional[str] = None, 73 ): 74 """Initialize artifact store. 75 76 Args: 77 project: Project name. 78 version: Version string (e.g., "1.2.0"). 79 backend: Storage backend ("local", "s3", "mlflow", "gdc") or custom ArtifactBackend instance. 80 Defaults to value from ~/.geronimo/config.yaml. 81 base_path: Base path for local storage. 82 Defaults to ~/.geronimo/artifacts. 83 s3_bucket: S3 bucket for s3 backend. 84 Defaults to value from config or GERONIMO_ARTIFACT_BUCKET env var. 85 """ 86 # Load user config for defaults 87 from geronimo.config.user_config import load_user_config 88 user_config = load_user_config() 89 90 self.project = project 91 self.version = version 92 93 # Handle custom backend instance 94 if isinstance(backend, ArtifactBackend): 95 self.backend = "custom" 96 self._backend_impl = backend 97 else: 98 self.backend = backend or user_config.artifacts.backend 99 100 # Resolve s3_bucket with fallback chain: param -> config -> env -> default 101 self.s3_bucket = ( 102 s3_bucket 103 or user_config.artifacts.s3_bucket 104 or os.getenv("GERONIMO_ARTIFACT_BUCKET", "ml-artifacts") 105 ) 106 107 # Resolve base_path 108 self.base_path = base_path or os.path.expanduser(user_config.artifacts.base_path) 109 110 # Create backend instance using factory 111 self._backend_impl = self._create_backend() 112 113 self._metadata: dict[str, ArtifactMetadata] = {} 114 115 def _create_backend(self) -> ArtifactBackend: 116 """Factory method to create backend instance. 117 118 Returns: 119 ArtifactBackend implementation based on self.backend type. 120 """ 121 if self.backend == "local": 122 from geronimo.artifacts.local_backend import LocalArtifactBackend 123 return LocalArtifactBackend( 124 project=self.project, 125 version=self.version, 126 base_path=self.base_path, 127 ) 128 elif self.backend == "s3": 129 from geronimo.artifacts.s3_backend import S3ArtifactBackend 130 return S3ArtifactBackend( 131 project=self.project, 132 version=self.version, 133 bucket=self.s3_bucket, 134 ) 135 elif self.backend == "gdc": 136 from geronimo.artifacts.gdc_backend import GeronimoDeployCloudArtifactBackend 137 return GeronimoDeployCloudArtifactBackend( 138 project=self.project, 139 version=self.version, 140 ) 141 elif self.backend == "mlflow": 142 from geronimo.artifacts.mlflow_backend import MLFlowArtifactBackend 143 return MLFlowArtifactBackend( 144 project=self.project, 145 version=self.version, 146 ) 147 else: 148 raise ValueError(f"Unknown backend type: {self.backend}") 149 150 @classmethod 151 def load( 152 cls, 153 project: str, 154 version: str, 155 backend: Optional[Literal["local", "s3", "gdc", "mlflow"]] = None, 156 **kwargs, 157 ) -> "ArtifactStore": 158 """Load existing artifact store. 159 160 Args: 161 project: Project name. 162 version: Version string. 163 backend: Storage backend. Defaults to value from config. 164 **kwargs: Additional backend options. 165 166 Returns: 167 ArtifactStore instance with loaded metadata. 168 """ 169 store = cls(project=project, version=version, backend=backend, **kwargs) 170 store._load_metadata() 171 return store 172 173 def save( 174 self, 175 name: str, 176 artifact: Any, 177 artifact_type: Optional[str] = None, 178 tags: Optional[dict[str, str]] = None, 179 ) -> str: 180 """Save an artifact. 181 182 Args: 183 name: Artifact name (e.g., "model", "encoder"). 184 artifact: Python object to serialize. 185 artifact_type: Optional type hint (auto-detected if not provided). 186 tags: Optional metadata tags. 187 188 Returns: 189 Path or URI where artifact was saved. 190 """ 191 artifact_type = artifact_type or type(artifact).__name__ 192 tags = tags or {} 193 194 meta_dict = { 195 "artifact_type": artifact_type, 196 "tags": tags, 197 } 198 199 # Delegate to backend 200 uri = self._backend_impl.save(name, artifact, meta_dict) 201 202 # Keep local metadata cache in sync 203 self._metadata[name] = ArtifactMetadata( 204 name=name, 205 version=self.version, 206 artifact_type=artifact_type, 207 created_at=datetime.utcnow(), 208 size_bytes=0, # Backend may have actual size 209 tags=tags, 210 ) 211 212 return uri 213 214 def get(self, name: str) -> Any: 215 """Load an artifact by name. 216 217 Args: 218 name: Artifact name. 219 220 Returns: 221 Deserialized artifact. 222 223 Raises: 224 KeyError: If artifact not found. 225 """ 226 return self._backend_impl.load(name) 227 228 def list(self) -> list[ArtifactMetadata]: 229 """List all artifacts in this store. 230 231 Returns: 232 List of artifact metadata. 233 """ 234 self._load_metadata() 235 return list(self._metadata.values()) 236 237 def delete(self, name: str) -> None: 238 """Delete an artifact. 239 240 Args: 241 name: Artifact name to delete 242 """ 243 self._backend_impl.delete(name) 244 if name in self._metadata: 245 del self._metadata[name] 246 247 def _load_metadata(self) -> None: 248 """Load metadata from backend.""" 249 # Try to get metadata from backend if it supports it 250 if hasattr(self._backend_impl, 'get_metadata_index'): 251 data = self._backend_impl.get_metadata_index() 252 self._metadata = { 253 k: ArtifactMetadata.model_validate(v) for k, v in data.items() 254 } 255 256 def __repr__(self) -> str: 257 return f"ArtifactStore({self.project}@{self.version}, backend={self.backend})"
Versioned storage for ML artifacts.
Supports local filesystem, S3, and Geronimo Cloud backends.
Reads defaults from ~/.geronimo/config.yaml (run geronimo config show).
Example:
from geronimo.artifacts import ArtifactStore # Save during training (uses global config defaults) store = ArtifactStore(project="credit-risk", version="1.2.0") store.save("model", trained_model) store.save("encoder", fitted_encoder) # Load in production store = ArtifactStore.load(project="credit-risk", version="1.2.0") model = store.get("model") encoder = store.get("encoder")
Configuration:
Run
geronimo config initto set up your preferred backend. Rungeronimo config showto view current settings.
66 def __init__( 67 self, 68 project: str, 69 version: str, 70 backend: Optional[Union[Literal["local", "s3", "gdc", "mlflow"], ArtifactBackend]] = None, 71 base_path: Optional[str] = None, 72 s3_bucket: Optional[str] = None, 73 ): 74 """Initialize artifact store. 75 76 Args: 77 project: Project name. 78 version: Version string (e.g., "1.2.0"). 79 backend: Storage backend ("local", "s3", "mlflow", "gdc") or custom ArtifactBackend instance. 80 Defaults to value from ~/.geronimo/config.yaml. 81 base_path: Base path for local storage. 82 Defaults to ~/.geronimo/artifacts. 83 s3_bucket: S3 bucket for s3 backend. 84 Defaults to value from config or GERONIMO_ARTIFACT_BUCKET env var. 85 """ 86 # Load user config for defaults 87 from geronimo.config.user_config import load_user_config 88 user_config = load_user_config() 89 90 self.project = project 91 self.version = version 92 93 # Handle custom backend instance 94 if isinstance(backend, ArtifactBackend): 95 self.backend = "custom" 96 self._backend_impl = backend 97 else: 98 self.backend = backend or user_config.artifacts.backend 99 100 # Resolve s3_bucket with fallback chain: param -> config -> env -> default 101 self.s3_bucket = ( 102 s3_bucket 103 or user_config.artifacts.s3_bucket 104 or os.getenv("GERONIMO_ARTIFACT_BUCKET", "ml-artifacts") 105 ) 106 107 # Resolve base_path 108 self.base_path = base_path or os.path.expanduser(user_config.artifacts.base_path) 109 110 # Create backend instance using factory 111 self._backend_impl = self._create_backend() 112 113 self._metadata: dict[str, ArtifactMetadata] = {}
Initialize artifact store.
Arguments:
- project: Project name.
- version: Version string (e.g., "1.2.0").
- backend: Storage backend ("local", "s3", "mlflow", "gdc") or custom ArtifactBackend instance. Defaults to value from ~/.geronimo/config.yaml.
- base_path: Base path for local storage. Defaults to ~/.geronimo/artifacts.
- s3_bucket: S3 bucket for s3 backend. Defaults to value from config or GERONIMO_ARTIFACT_BUCKET env var.
150 @classmethod 151 def load( 152 cls, 153 project: str, 154 version: str, 155 backend: Optional[Literal["local", "s3", "gdc", "mlflow"]] = None, 156 **kwargs, 157 ) -> "ArtifactStore": 158 """Load existing artifact store. 159 160 Args: 161 project: Project name. 162 version: Version string. 163 backend: Storage backend. Defaults to value from config. 164 **kwargs: Additional backend options. 165 166 Returns: 167 ArtifactStore instance with loaded metadata. 168 """ 169 store = cls(project=project, version=version, backend=backend, **kwargs) 170 store._load_metadata() 171 return store
Load existing artifact store.
Arguments:
- project: Project name.
- version: Version string.
- backend: Storage backend. Defaults to value from config.
- **kwargs: Additional backend options.
Returns:
ArtifactStore instance with loaded metadata.
173 def save( 174 self, 175 name: str, 176 artifact: Any, 177 artifact_type: Optional[str] = None, 178 tags: Optional[dict[str, str]] = None, 179 ) -> str: 180 """Save an artifact. 181 182 Args: 183 name: Artifact name (e.g., "model", "encoder"). 184 artifact: Python object to serialize. 185 artifact_type: Optional type hint (auto-detected if not provided). 186 tags: Optional metadata tags. 187 188 Returns: 189 Path or URI where artifact was saved. 190 """ 191 artifact_type = artifact_type or type(artifact).__name__ 192 tags = tags or {} 193 194 meta_dict = { 195 "artifact_type": artifact_type, 196 "tags": tags, 197 } 198 199 # Delegate to backend 200 uri = self._backend_impl.save(name, artifact, meta_dict) 201 202 # Keep local metadata cache in sync 203 self._metadata[name] = ArtifactMetadata( 204 name=name, 205 version=self.version, 206 artifact_type=artifact_type, 207 created_at=datetime.utcnow(), 208 size_bytes=0, # Backend may have actual size 209 tags=tags, 210 ) 211 212 return uri
Save an artifact.
Arguments:
- name: Artifact name (e.g., "model", "encoder").
- artifact: Python object to serialize.
- artifact_type: Optional type hint (auto-detected if not provided).
- tags: Optional metadata tags.
Returns:
Path or URI where artifact was saved.
214 def get(self, name: str) -> Any: 215 """Load an artifact by name. 216 217 Args: 218 name: Artifact name. 219 220 Returns: 221 Deserialized artifact. 222 223 Raises: 224 KeyError: If artifact not found. 225 """ 226 return self._backend_impl.load(name)
Load an artifact by name.
Arguments:
- name: Artifact name.
Returns:
Deserialized artifact.
Raises:
- KeyError: If artifact not found.
228 def list(self) -> list[ArtifactMetadata]: 229 """List all artifacts in this store. 230 231 Returns: 232 List of artifact metadata. 233 """ 234 self._load_metadata() 235 return list(self._metadata.values())
List all artifacts in this store.
Returns:
List of artifact metadata.
237 def delete(self, name: str) -> None: 238 """Delete an artifact. 239 240 Args: 241 name: Artifact name to delete 242 """ 243 self._backend_impl.delete(name) 244 if name in self._metadata: 245 del self._metadata[name]
Delete an artifact.
Arguments:
- name: Artifact name to delete
8@runtime_checkable 9class ArtifactBackend(Protocol): 10 """Protocol for custom artifact storage backends. 11 12 Implement this protocol to use your own storage system 13 (internal object store, custom S3, etc.) with Geronimo. 14 15 Example: 16 class MyCompanyStore(ArtifactBackend): 17 def save(self, name, artifact, metadata): 18 return self._internal_api.upload(artifact) 19 20 def load(self, uri): 21 return self._internal_api.download(uri) 22 23 def list(self, prefix): 24 return self._internal_api.list_objects(prefix) 25 26 def delete(self, uri): 27 self._internal_api.delete(uri) 28 29 Usage: 30 from geronimo.artifacts import ArtifactStore 31 32 custom_backend = MyCompanyStore(config=my_config) 33 store = ArtifactStore( 34 project="my-model", 35 version="1.0.0", 36 backend=custom_backend, 37 ) 38 """ 39 40 def save(self, name: str, artifact: Any, metadata: dict) -> str: 41 """Save an artifact. 42 43 Args: 44 name: Artifact name (e.g., "model", "encoder") 45 artifact: Serialized artifact bytes 46 metadata: Artifact metadata dict 47 48 Returns: 49 URI or path where artifact was saved 50 """ 51 ... 52 53 def load(self, uri: str) -> Any: 54 """Load an artifact by URI. 55 56 Args: 57 uri: URI returned from save() 58 59 Returns: 60 Deserialized artifact 61 """ 62 ... 63 64 def list(self, prefix: str) -> list[str]: 65 """List artifacts matching prefix. 66 67 Args: 68 prefix: Prefix to filter artifacts 69 70 Returns: 71 List of artifact URIs 72 """ 73 ... 74 75 def delete(self, uri: str) -> None: 76 """Delete an artifact. 77 78 Args: 79 uri: URI of artifact to delete 80 """ 81 ...
Protocol for custom artifact storage backends.
Implement this protocol to use your own storage system (internal object store, custom S3, etc.) with Geronimo.
Example:
class MyCompanyStore(ArtifactBackend): def save(self, name, artifact, metadata): return self._internal_api.upload(artifact)
def load(self, uri): return self._internal_api.download(uri) def list(self, prefix): return self._internal_api.list_objects(prefix) def delete(self, uri): self._internal_api.delete(uri)
Usage:
from geronimo.artifacts import ArtifactStore
custom_backend = MyCompanyStore(config=my_config) store = ArtifactStore( project="my-model", version="1.0.0", backend=custom_backend, )
1953def _no_init_or_replace_init(self, *args, **kwargs): 1954 cls = type(self) 1955 1956 if cls._is_protocol: 1957 raise TypeError('Protocols cannot be instantiated') 1958 1959 # Already using a custom `__init__`. No need to calculate correct 1960 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1961 if cls.__init__ is not _no_init_or_replace_init: 1962 return 1963 1964 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1965 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1966 # searches for a proper new `__init__` in the MRO. The new `__init__` 1967 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1968 # instantiation of the protocol subclass will thus use the new 1969 # `__init__` and no longer call `_no_init_or_replace_init`. 1970 for base in cls.__mro__: 1971 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1972 if init is not _no_init_or_replace_init: 1973 cls.__init__ = init 1974 break 1975 else: 1976 # should not happen 1977 cls.__init__ = object.__init__ 1978 1979 cls.__init__(self, *args, **kwargs)
40 def save(self, name: str, artifact: Any, metadata: dict) -> str: 41 """Save an artifact. 42 43 Args: 44 name: Artifact name (e.g., "model", "encoder") 45 artifact: Serialized artifact bytes 46 metadata: Artifact metadata dict 47 48 Returns: 49 URI or path where artifact was saved 50 """ 51 ...
Save an artifact.
Arguments:
- name: Artifact name (e.g., "model", "encoder")
- artifact: Serialized artifact bytes
- metadata: Artifact metadata dict
Returns:
URI or path where artifact was saved
53 def load(self, uri: str) -> Any: 54 """Load an artifact by URI. 55 56 Args: 57 uri: URI returned from save() 58 59 Returns: 60 Deserialized artifact 61 """ 62 ...
Load an artifact by URI.
Arguments:
- uri: URI returned from save()
Returns:
Deserialized artifact
64 def list(self, prefix: str) -> list[str]: 65 """List artifacts matching prefix. 66 67 Args: 68 prefix: Prefix to filter artifacts 69 70 Returns: 71 List of artifact URIs 72 """ 73 ...
List artifacts matching prefix.
Arguments:
- prefix: Prefix to filter artifacts
Returns:
List of artifact URIs
17class LocalArtifactBackend(ArtifactBackend): 18 """Artifact backend for local filesystem storage. 19 20 Stores artifacts as pickle files with JSON metadata index. 21 """ 22 23 project: str 24 """The project name.""" 25 26 version: str 27 """The version string.""" 28 29 base_path: Path 30 """Base directory for artifacts.""" 31 32 artifact_path: Path 33 """Directory for the specific project version.""" 34 35 _metadata_file: Path 36 """Path to the metadata index file.""" 37 38 def __init__( 39 self, 40 project: str, 41 version: str, 42 base_path: Optional[str] = None, 43 ): 44 """Initialize local backend. 45 46 Args: 47 project: Project name. 48 version: Version string. 49 base_path: Base directory for artifacts. 50 Defaults to ~/.geronimo/artifacts. 51 """ 52 self.project = project 53 self.version = version 54 self.base_path = Path(base_path or os.path.expanduser("~/.geronimo/artifacts")) 55 self.artifact_path = self.base_path / project / version 56 self.artifact_path.mkdir(parents=True, exist_ok=True) 57 self._metadata_file = self.artifact_path / "metadata.json" 58 59 def save(self, name: str, artifact: Any, metadata: dict) -> str: 60 """Save an artifact to local filesystem. 61 62 Args: 63 name: Artifact name. 64 artifact: Python object to serialize. 65 metadata: Artifact metadata dict. 66 67 Returns: 68 Path to saved artifact file. 69 """ 70 artifact_file = self.artifact_path / f"{name}.pkl" 71 72 with open(artifact_file, "wb") as f: 73 pickle.dump(artifact, f) 74 75 logger.debug(f"Saved artifact '{name}' to {artifact_file}") 76 77 # Update metadata index 78 self._save_to_metadata_index(name, artifact_file.stat().st_size, metadata) 79 80 return str(artifact_file) 81 82 def load(self, uri: str) -> Any: 83 """Load an artifact by name or path. 84 85 Args: 86 uri: Artifact name or file path. 87 88 Returns: 89 Deserialized artifact. 90 91 Raises: 92 KeyError: If artifact not found. 93 """ 94 # If path, use directly; otherwise treat as name 95 if os.path.isabs(uri) or os.path.exists(uri): 96 artifact_file = Path(uri) 97 else: 98 artifact_file = self.artifact_path / f"{uri}.pkl" 99 100 if not artifact_file.exists(): 101 raise KeyError(f"Artifact not found: {uri}") 102 103 logger.debug(f"Loading artifact from {artifact_file}") 104 105 with open(artifact_file, "rb") as f: 106 return pickle.load(f) 107 108 def list(self, prefix: Optional[str] = None) -> list[str]: 109 """List artifacts in the store. 110 111 Args: 112 prefix: Optional name prefix to filter. 113 114 Returns: 115 List of artifact file paths. 116 """ 117 paths = [] 118 for pkl_file in self.artifact_path.glob("*.pkl"): 119 name = pkl_file.stem 120 if prefix is None or name.startswith(prefix): 121 paths.append(str(pkl_file)) 122 return paths 123 124 def delete(self, uri: str) -> None: 125 """Delete an artifact. 126 127 Args: 128 uri: Artifact name or file path. 129 """ 130 # If path, use directly; otherwise treat as name 131 if os.path.isabs(uri) or os.path.exists(uri): 132 artifact_file = Path(uri) 133 name = artifact_file.stem 134 else: 135 name = uri 136 artifact_file = self.artifact_path / f"{uri}.pkl" 137 138 if artifact_file.exists(): 139 artifact_file.unlink() 140 logger.info(f"Deleted artifact '{name}'") 141 self._remove_from_metadata_index(name) 142 else: 143 logger.warning(f"Artifact not found for deletion: {uri}") 144 145 def _save_to_metadata_index(self, name: str, size_bytes: int, metadata: dict) -> None: 146 """Add artifact to metadata index.""" 147 index = self._load_metadata_index() 148 149 index[name] = { 150 "name": name, 151 "version": self.version, 152 "artifact_type": metadata.get("artifact_type", "unknown"), 153 "created_at": datetime.utcnow().isoformat(), 154 "size_bytes": size_bytes, 155 "tags": metadata.get("tags", {}), 156 } 157 158 self._metadata_file.write_text(json.dumps(index, indent=2)) 159 160 def _remove_from_metadata_index(self, name: str) -> None: 161 """Remove artifact from metadata index.""" 162 index = self._load_metadata_index() 163 if name in index: 164 del index[name] 165 self._metadata_file.write_text(json.dumps(index, indent=2)) 166 167 def _load_metadata_index(self) -> dict: 168 """Load metadata index from file.""" 169 if self._metadata_file.exists(): 170 return json.loads(self._metadata_file.read_text()) 171 return {} 172 173 def get_metadata_index(self) -> dict: 174 """Get the full metadata index for this store. 175 176 Returns: 177 Dict mapping artifact names to metadata dicts. 178 """ 179 return self._load_metadata_index()
Artifact backend for local filesystem storage.
Stores artifacts as pickle files with JSON metadata index.
38 def __init__( 39 self, 40 project: str, 41 version: str, 42 base_path: Optional[str] = None, 43 ): 44 """Initialize local backend. 45 46 Args: 47 project: Project name. 48 version: Version string. 49 base_path: Base directory for artifacts. 50 Defaults to ~/.geronimo/artifacts. 51 """ 52 self.project = project 53 self.version = version 54 self.base_path = Path(base_path or os.path.expanduser("~/.geronimo/artifacts")) 55 self.artifact_path = self.base_path / project / version 56 self.artifact_path.mkdir(parents=True, exist_ok=True) 57 self._metadata_file = self.artifact_path / "metadata.json"
Initialize local backend.
Arguments:
- project: Project name.
- version: Version string.
- base_path: Base directory for artifacts. Defaults to ~/.geronimo/artifacts.
59 def save(self, name: str, artifact: Any, metadata: dict) -> str: 60 """Save an artifact to local filesystem. 61 62 Args: 63 name: Artifact name. 64 artifact: Python object to serialize. 65 metadata: Artifact metadata dict. 66 67 Returns: 68 Path to saved artifact file. 69 """ 70 artifact_file = self.artifact_path / f"{name}.pkl" 71 72 with open(artifact_file, "wb") as f: 73 pickle.dump(artifact, f) 74 75 logger.debug(f"Saved artifact '{name}' to {artifact_file}") 76 77 # Update metadata index 78 self._save_to_metadata_index(name, artifact_file.stat().st_size, metadata) 79 80 return str(artifact_file)
Save an artifact to local filesystem.
Arguments:
- name: Artifact name.
- artifact: Python object to serialize.
- metadata: Artifact metadata dict.
Returns:
Path to saved artifact file.
82 def load(self, uri: str) -> Any: 83 """Load an artifact by name or path. 84 85 Args: 86 uri: Artifact name or file path. 87 88 Returns: 89 Deserialized artifact. 90 91 Raises: 92 KeyError: If artifact not found. 93 """ 94 # If path, use directly; otherwise treat as name 95 if os.path.isabs(uri) or os.path.exists(uri): 96 artifact_file = Path(uri) 97 else: 98 artifact_file = self.artifact_path / f"{uri}.pkl" 99 100 if not artifact_file.exists(): 101 raise KeyError(f"Artifact not found: {uri}") 102 103 logger.debug(f"Loading artifact from {artifact_file}") 104 105 with open(artifact_file, "rb") as f: 106 return pickle.load(f)
Load an artifact by name or path.
Arguments:
- uri: Artifact name or file path.
Returns:
Deserialized artifact.
Raises:
- KeyError: If artifact not found.
108 def list(self, prefix: Optional[str] = None) -> list[str]: 109 """List artifacts in the store. 110 111 Args: 112 prefix: Optional name prefix to filter. 113 114 Returns: 115 List of artifact file paths. 116 """ 117 paths = [] 118 for pkl_file in self.artifact_path.glob("*.pkl"): 119 name = pkl_file.stem 120 if prefix is None or name.startswith(prefix): 121 paths.append(str(pkl_file)) 122 return paths
List artifacts in the store.
Arguments:
- prefix: Optional name prefix to filter.
Returns:
List of artifact file paths.
124 def delete(self, uri: str) -> None: 125 """Delete an artifact. 126 127 Args: 128 uri: Artifact name or file path. 129 """ 130 # If path, use directly; otherwise treat as name 131 if os.path.isabs(uri) or os.path.exists(uri): 132 artifact_file = Path(uri) 133 name = artifact_file.stem 134 else: 135 name = uri 136 artifact_file = self.artifact_path / f"{uri}.pkl" 137 138 if artifact_file.exists(): 139 artifact_file.unlink() 140 logger.info(f"Deleted artifact '{name}'") 141 self._remove_from_metadata_index(name) 142 else: 143 logger.warning(f"Artifact not found for deletion: {uri}")
Delete an artifact.
Arguments:
- uri: Artifact name or file path.
173 def get_metadata_index(self) -> dict: 174 """Get the full metadata index for this store. 175 176 Returns: 177 Dict mapping artifact names to metadata dicts. 178 """ 179 return self._load_metadata_index()
Get the full metadata index for this store.
Returns:
Dict mapping artifact names to metadata dicts.
17class S3ArtifactBackend(ArtifactBackend): 18 """Artifact backend for S3 storage. 19 20 Stores artifacts as pickle files in S3 with JSON metadata index. 21 """ 22 23 project: str 24 """The project name.""" 25 26 version: str 27 """The version string.""" 28 29 bucket: str 30 """The S3 bucket name.""" 31 32 _prefix: str 33 """The S3 key prefix for this project version.""" 34 35 def __init__( 36 self, 37 project: str, 38 version: str, 39 bucket: Optional[str] = None, 40 ): 41 """Initialize S3 backend. 42 43 Args: 44 project: Project name. 45 version: Version string. 46 bucket: S3 bucket name. Defaults to GERONIMO_ARTIFACT_BUCKET env var. 47 """ 48 self.project = project 49 self.version = version 50 self.bucket = bucket or os.getenv("GERONIMO_ARTIFACT_BUCKET", "ml-artifacts") 51 self._prefix = f"{project}/{version}" 52 53 def _get_s3_client(self): 54 """Get boto3 S3 client (lazy import).""" 55 import boto3 56 return boto3.client("s3") 57 58 def save(self, name: str, artifact: Any, metadata: dict) -> str: 59 """Save an artifact to S3. 60 61 Args: 62 name: Artifact name. 63 artifact: Python object to serialize. 64 metadata: Artifact metadata dict. 65 66 Returns: 67 S3 URI of saved artifact. 68 """ 69 s3 = self._get_s3_client() 70 key = f"{self._prefix}/{name}.pkl" 71 72 # Serialize to temp file 73 with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: 74 pickle.dump(artifact, f) 75 temp_path = f.name 76 77 try: 78 s3.upload_file(temp_path, self.bucket, key) 79 size_bytes = os.path.getsize(temp_path) 80 logger.debug(f"Uploaded artifact '{name}' to s3://{self.bucket}/{key}") 81 finally: 82 os.unlink(temp_path) 83 84 # Update metadata index in S3 85 self._save_to_metadata_index(name, size_bytes, metadata) 86 87 return f"s3://{self.bucket}/{key}" 88 89 def load(self, uri: str) -> Any: 90 """Load an artifact by name or S3 URI. 91 92 Args: 93 uri: Artifact name or S3 URI. 94 95 Returns: 96 Deserialized artifact. 97 98 Raises: 99 KeyError: If artifact not found. 100 """ 101 s3 = self._get_s3_client() 102 103 # Parse URI or construct from name 104 if uri.startswith("s3://"): 105 # Parse s3://bucket/path/to/artifact.pkl 106 path = uri.replace("s3://", "") 107 parts = path.split("/", 1) 108 bucket = parts[0] 109 key = parts[1] if len(parts) > 1 else "" 110 else: 111 bucket = self.bucket 112 key = f"{self._prefix}/{uri}.pkl" 113 114 with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: 115 temp_path = f.name 116 117 try: 118 s3.download_file(bucket, key, temp_path) 119 logger.debug(f"Downloaded artifact from s3://{bucket}/{key}") 120 121 with open(temp_path, "rb") as f: 122 return pickle.load(f) 123 except s3.exceptions.NoSuchKey: 124 raise KeyError(f"Artifact not found: {uri}") 125 finally: 126 if os.path.exists(temp_path): 127 os.unlink(temp_path) 128 129 def list(self, prefix: Optional[str] = None) -> list[str]: 130 """List artifacts in the store. 131 132 Args: 133 prefix: Optional name prefix to filter. 134 135 Returns: 136 List of S3 URIs. 137 """ 138 s3 = self._get_s3_client() 139 140 search_prefix = self._prefix 141 if prefix: 142 search_prefix = f"{search_prefix}/{prefix}" 143 144 uris = [] 145 paginator = s3.get_paginator("list_objects_v2") 146 147 for page in paginator.paginate(Bucket=self.bucket, Prefix=search_prefix): 148 for obj in page.get("Contents", []): 149 key = obj["Key"] 150 if key.endswith(".pkl"): 151 uris.append(f"s3://{self.bucket}/{key}") 152 153 return uris 154 155 def delete(self, uri: str) -> None: 156 """Delete an artifact. 157 158 Args: 159 uri: Artifact name or S3 URI. 160 """ 161 s3 = self._get_s3_client() 162 163 # Parse URI or construct from name 164 if uri.startswith("s3://"): 165 path = uri.replace("s3://", "") 166 parts = path.split("/", 1) 167 bucket = parts[0] 168 key = parts[1] if len(parts) > 1 else "" 169 # Extract name from key for metadata 170 name = key.split("/")[-1].replace(".pkl", "") 171 else: 172 name = uri 173 bucket = self.bucket 174 key = f"{self._prefix}/{uri}.pkl" 175 176 try: 177 s3.delete_object(Bucket=bucket, Key=key) 178 logger.info(f"Deleted artifact from s3://{bucket}/{key}") 179 self._remove_from_metadata_index(name) 180 except Exception as e: 181 logger.warning(f"Failed to delete artifact: {e}") 182 183 def _save_to_metadata_index(self, name: str, size_bytes: int, metadata: dict) -> None: 184 """Add artifact to metadata index in S3.""" 185 index = self._load_metadata_index() 186 187 index[name] = { 188 "name": name, 189 "version": self.version, 190 "artifact_type": metadata.get("artifact_type", "unknown"), 191 "created_at": datetime.utcnow().isoformat(), 192 "size_bytes": size_bytes, 193 "tags": metadata.get("tags", {}), 194 } 195 196 s3 = self._get_s3_client() 197 key = f"{self._prefix}/metadata.json" 198 s3.put_object( 199 Bucket=self.bucket, 200 Key=key, 201 Body=json.dumps(index, indent=2), 202 ContentType="application/json", 203 ) 204 205 def _remove_from_metadata_index(self, name: str) -> None: 206 """Remove artifact from metadata index.""" 207 index = self._load_metadata_index() 208 if name in index: 209 del index[name] 210 s3 = self._get_s3_client() 211 key = f"{self._prefix}/metadata.json" 212 s3.put_object( 213 Bucket=self.bucket, 214 Key=key, 215 Body=json.dumps(index, indent=2), 216 ContentType="application/json", 217 ) 218 219 def _load_metadata_index(self) -> dict: 220 """Load metadata index from S3.""" 221 s3 = self._get_s3_client() 222 key = f"{self._prefix}/metadata.json" 223 224 try: 225 response = s3.get_object(Bucket=self.bucket, Key=key) 226 return json.loads(response["Body"].read()) 227 except Exception: 228 return {} 229 230 def get_metadata_index(self) -> dict: 231 """Get the full metadata index for this store. 232 233 Returns: 234 Dict mapping artifact names to metadata dicts. 235 """ 236 return self._load_metadata_index()
Artifact backend for S3 storage.
Stores artifacts as pickle files in S3 with JSON metadata index.
35 def __init__( 36 self, 37 project: str, 38 version: str, 39 bucket: Optional[str] = None, 40 ): 41 """Initialize S3 backend. 42 43 Args: 44 project: Project name. 45 version: Version string. 46 bucket: S3 bucket name. Defaults to GERONIMO_ARTIFACT_BUCKET env var. 47 """ 48 self.project = project 49 self.version = version 50 self.bucket = bucket or os.getenv("GERONIMO_ARTIFACT_BUCKET", "ml-artifacts") 51 self._prefix = f"{project}/{version}"
Initialize S3 backend.
Arguments:
- project: Project name.
- version: Version string.
- bucket: S3 bucket name. Defaults to GERONIMO_ARTIFACT_BUCKET env var.
58 def save(self, name: str, artifact: Any, metadata: dict) -> str: 59 """Save an artifact to S3. 60 61 Args: 62 name: Artifact name. 63 artifact: Python object to serialize. 64 metadata: Artifact metadata dict. 65 66 Returns: 67 S3 URI of saved artifact. 68 """ 69 s3 = self._get_s3_client() 70 key = f"{self._prefix}/{name}.pkl" 71 72 # Serialize to temp file 73 with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: 74 pickle.dump(artifact, f) 75 temp_path = f.name 76 77 try: 78 s3.upload_file(temp_path, self.bucket, key) 79 size_bytes = os.path.getsize(temp_path) 80 logger.debug(f"Uploaded artifact '{name}' to s3://{self.bucket}/{key}") 81 finally: 82 os.unlink(temp_path) 83 84 # Update metadata index in S3 85 self._save_to_metadata_index(name, size_bytes, metadata) 86 87 return f"s3://{self.bucket}/{key}"
Save an artifact to S3.
Arguments:
- name: Artifact name.
- artifact: Python object to serialize.
- metadata: Artifact metadata dict.
Returns:
S3 URI of saved artifact.
89 def load(self, uri: str) -> Any: 90 """Load an artifact by name or S3 URI. 91 92 Args: 93 uri: Artifact name or S3 URI. 94 95 Returns: 96 Deserialized artifact. 97 98 Raises: 99 KeyError: If artifact not found. 100 """ 101 s3 = self._get_s3_client() 102 103 # Parse URI or construct from name 104 if uri.startswith("s3://"): 105 # Parse s3://bucket/path/to/artifact.pkl 106 path = uri.replace("s3://", "") 107 parts = path.split("/", 1) 108 bucket = parts[0] 109 key = parts[1] if len(parts) > 1 else "" 110 else: 111 bucket = self.bucket 112 key = f"{self._prefix}/{uri}.pkl" 113 114 with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: 115 temp_path = f.name 116 117 try: 118 s3.download_file(bucket, key, temp_path) 119 logger.debug(f"Downloaded artifact from s3://{bucket}/{key}") 120 121 with open(temp_path, "rb") as f: 122 return pickle.load(f) 123 except s3.exceptions.NoSuchKey: 124 raise KeyError(f"Artifact not found: {uri}") 125 finally: 126 if os.path.exists(temp_path): 127 os.unlink(temp_path)
Load an artifact by name or S3 URI.
Arguments:
- uri: Artifact name or S3 URI.
Returns:
Deserialized artifact.
Raises:
- KeyError: If artifact not found.
129 def list(self, prefix: Optional[str] = None) -> list[str]: 130 """List artifacts in the store. 131 132 Args: 133 prefix: Optional name prefix to filter. 134 135 Returns: 136 List of S3 URIs. 137 """ 138 s3 = self._get_s3_client() 139 140 search_prefix = self._prefix 141 if prefix: 142 search_prefix = f"{search_prefix}/{prefix}" 143 144 uris = [] 145 paginator = s3.get_paginator("list_objects_v2") 146 147 for page in paginator.paginate(Bucket=self.bucket, Prefix=search_prefix): 148 for obj in page.get("Contents", []): 149 key = obj["Key"] 150 if key.endswith(".pkl"): 151 uris.append(f"s3://{self.bucket}/{key}") 152 153 return uris
List artifacts in the store.
Arguments:
- prefix: Optional name prefix to filter.
Returns:
List of S3 URIs.
155 def delete(self, uri: str) -> None: 156 """Delete an artifact. 157 158 Args: 159 uri: Artifact name or S3 URI. 160 """ 161 s3 = self._get_s3_client() 162 163 # Parse URI or construct from name 164 if uri.startswith("s3://"): 165 path = uri.replace("s3://", "") 166 parts = path.split("/", 1) 167 bucket = parts[0] 168 key = parts[1] if len(parts) > 1 else "" 169 # Extract name from key for metadata 170 name = key.split("/")[-1].replace(".pkl", "") 171 else: 172 name = uri 173 bucket = self.bucket 174 key = f"{self._prefix}/{uri}.pkl" 175 176 try: 177 s3.delete_object(Bucket=bucket, Key=key) 178 logger.info(f"Deleted artifact from s3://{bucket}/{key}") 179 self._remove_from_metadata_index(name) 180 except Exception as e: 181 logger.warning(f"Failed to delete artifact: {e}")
Delete an artifact.
Arguments:
- uri: Artifact name or S3 URI.
230 def get_metadata_index(self) -> dict: 231 """Get the full metadata index for this store. 232 233 Returns: 234 Dict mapping artifact names to metadata dicts. 235 """ 236 return self._load_metadata_index()
Get the full metadata index for this store.
Returns:
Dict mapping artifact names to metadata dicts.
17class GeronimoDeployCloudArtifactBackend(ArtifactBackend): 18 """Artifact backend for Geronimo Deploy Cloud. 19 20 Stores artifacts using the Geronimo Deploy Cloud API with support for 21 cross-user access via namespaces. 22 """ 23 24 project: Optional[str] 25 """The project name.""" 26 27 version: Optional[str] 28 """The project version.""" 29 30 namespace: Optional[str] 31 """Namespace for cross-user artifact access.""" 32 33 client: GeronimoCloudClient 34 """The configured cloud client.""" 35 36 def __init__( 37 self, 38 project: Optional[str] = None, 39 version: Optional[str] = None, 40 namespace: Optional[str] = None, 41 client: Optional[GeronimoCloudClient] = None, 42 ): 43 """Initialize the cloud backend. 44 45 Args: 46 project: Project name (optional context). 47 version: Project version (optional context). 48 namespace: Optional namespace for cross-user access. 49 client: Optional pre-configured client. 50 """ 51 self.project = project 52 self.version = version 53 self.namespace = namespace 54 self.client = client or GeronimoCloudClient() 55 56 def _check_auth(self) -> None: 57 """Check if client is authenticated.""" 58 if not self.client.token: 59 raise RuntimeError( 60 "Not authenticated. Run 'geronimo auth login' first." 61 ) 62 63 def _parse_artifact_uri(self, uri: str) -> tuple[str, str, str]: 64 """Parse S3 URI or artifact name to extract (project, version, name). 65 66 Args: 67 uri: S3 URI (s3://bucket/user/project/version/name.pkl) or artifact name 68 69 Returns: 70 Tuple of (project, version, name) 71 72 Raises: 73 ValueError: If URI format is invalid or context missing 74 """ 75 if uri.startswith("s3://"): 76 # Parse s3://{bucket}/{user_id}/{project}/{version}/{name}.pkl 77 parts = uri.replace("s3://", "").split("/") 78 if len(parts) >= 5: 79 return parts[2], parts[3], parts[4].replace(".pkl", "") 80 else: 81 raise ValueError( 82 f"Invalid S3 URI format: {uri}. " 83 f"Expected: s3://bucket/user/project/version/name" 84 ) 85 else: 86 # Treat as artifact name, use context 87 if not (self.project and self.version): 88 raise ValueError( 89 "Project and version context required when using artifact name. " 90 f"Got: project={self.project}, version={self.version}" 91 ) 92 return self.project, self.version, uri 93 94 def save(self, name: str, artifact: Any, metadata: dict) -> str: 95 """Save an artifact to the cloud. 96 97 Args: 98 name: Artifact name. 99 artifact: Python object to serialize. 100 metadata: Artifact metadata. 101 102 Returns: 103 S3 URI of the saved artifact. 104 105 Raises: 106 RuntimeError: If not authenticated. 107 httpx.HTTPError: If API request fails. 108 """ 109 self._check_auth() 110 111 # 1. Serialize artifact to get actual size 112 with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: 113 pickle.dump(artifact, f) 114 f.flush() 115 temp_path = f.name 116 size_bytes = os.path.getsize(temp_path) 117 118 try: 119 # 2. Get upload URL from cloud API 120 with api_client( 121 self.client.api_url, self.client.headers, operation=f"save/{name}" 122 ) as http: 123 logger.debug(f"Requesting upload URL for artifact '{name}'") 124 125 payload = { 126 "project": self.project, 127 "version": self.version, 128 "name": name, 129 "size_bytes": size_bytes, 130 "metadata": metadata, 131 } 132 if self.namespace: 133 payload["namespace"] = self.namespace 134 135 resp = http.post("/v1/artifacts/cloud-save", json=payload) 136 resp.raise_for_status() 137 data = resp.json() 138 upload_url = data["upload_url"] 139 artifact_id = data["id"] 140 s3_uri = data["s3_uri"] 141 142 logger.info(f"Uploading artifact '{name}' to cloud (ID: {artifact_id})") 143 144 # 3. Upload content to S3 145 with open(temp_path, "rb") as f: 146 with transfer_client(operation=f"upload/{name}") as http: 147 resp = http.put(upload_url, content=f) 148 resp.raise_for_status() 149 150 # 4. Confirm upload 151 with api_client( 152 self.client.api_url, self.client.headers, operation=f"confirm/{name}" 153 ) as http: 154 resp = http.post( 155 f"/v1/artifacts/{artifact_id}/confirm", 156 json={"size_bytes": size_bytes}, 157 ) 158 resp.raise_for_status() 159 160 logger.info(f"Successfully saved artifact '{name}' to {s3_uri}") 161 return s3_uri 162 163 finally: 164 if os.path.exists(temp_path): 165 os.unlink(temp_path) 166 167 def load(self, uri: str) -> Any: 168 """Load an artifact from the cloud. 169 170 Args: 171 uri: S3 URI (s3://bucket/user/project/version/name) or artifact name. 172 173 Returns: 174 Deserialized artifact. 175 176 Raises: 177 ValueError: If URI cannot be parsed or context is missing. 178 RuntimeError: If not authenticated. 179 httpx.HTTPError: If API request fails. 180 """ 181 self._check_auth() 182 183 project, version, name = self._parse_artifact_uri(uri) 184 185 # 1. Get download URL 186 with api_client( 187 self.client.api_url, self.client.headers, operation=f"load/{name}" 188 ) as http: 189 logger.debug(f"Requesting download URL for '{project}/{version}/{name}'") 190 191 payload = { 192 "project": project, 193 "version": version, 194 "name": name, 195 } 196 if self.namespace: 197 payload["namespace"] = self.namespace 198 199 resp = http.post("/v1/artifacts/cloud-load", json=payload) 200 resp.raise_for_status() 201 download_url = resp.json()["download_url"] 202 203 # 2. Download and deserialize 204 logger.info(f"Loading artifact '{name}' from cloud") 205 with transfer_client(operation=f"download/{name}") as http: 206 resp = http.get(download_url) 207 resp.raise_for_status() 208 return pickle.loads(resp.content) 209 210 def list(self, prefix: Optional[str] = None) -> list[str]: 211 """List artifacts. 212 213 Args: 214 prefix: Optional name prefix to filter results. 215 216 Returns: 217 List of artifact S3 URIs. 218 219 Raises: 220 RuntimeError: If not authenticated. 221 httpx.HTTPError: If API request fails. 222 """ 223 self._check_auth() 224 225 params = {} 226 if self.project: 227 params["project"] = self.project 228 if self.version: 229 params["version"] = self.version 230 if self.namespace: 231 params["namespace"] = self.namespace 232 if prefix: 233 params["prefix"] = prefix 234 235 with api_client( 236 self.client.api_url, self.client.headers, operation="list" 237 ) as http: 238 logger.debug(f"Listing artifacts with params: {params}") 239 resp = http.get("/v1/artifacts", params=params) 240 resp.raise_for_status() 241 artifacts = resp.json().get("artifacts", []) 242 return [a["s3_uri"] for a in artifacts] 243 244 def delete(self, uri: str) -> None: 245 """Delete an artifact. 246 247 Args: 248 uri: S3 URI or artifact name. 249 250 Raises: 251 ValueError: If URI cannot be parsed or context is missing. 252 RuntimeError: If not authenticated. 253 httpx.HTTPError: If API request fails. 254 """ 255 self._check_auth() 256 257 project, version, name = self._parse_artifact_uri(uri) 258 259 with api_client( 260 self.client.api_url, self.client.headers, operation=f"delete/{name}" 261 ) as http: 262 # Find artifact ID by searching 263 search_params = { 264 "project": project, 265 "version": version, 266 "name": name 267 } 268 if self.namespace: 269 search_params["namespace"] = self.namespace 270 271 logger.debug(f"Searching for artifact to delete: {search_params}") 272 resp = http.get("/v1/artifacts", params=search_params) 273 resp.raise_for_status() 274 matches = resp.json().get("artifacts", []) 275 276 if not matches: 277 logger.warning(f"Artifact '{name}' not found, skipping delete") 278 return 279 280 # Delete all matches 281 for artifact in matches: 282 artifact_id = artifact["id"] 283 logger.info(f"Deleting artifact '{name}' (ID: {artifact_id})") 284 resp = http.delete(f"/v1/artifacts/{artifact_id}") 285 resp.raise_for_status()
Artifact backend for Geronimo Deploy Cloud.
Stores artifacts using the Geronimo Deploy Cloud API with support for cross-user access via namespaces.
36 def __init__( 37 self, 38 project: Optional[str] = None, 39 version: Optional[str] = None, 40 namespace: Optional[str] = None, 41 client: Optional[GeronimoCloudClient] = None, 42 ): 43 """Initialize the cloud backend. 44 45 Args: 46 project: Project name (optional context). 47 version: Project version (optional context). 48 namespace: Optional namespace for cross-user access. 49 client: Optional pre-configured client. 50 """ 51 self.project = project 52 self.version = version 53 self.namespace = namespace 54 self.client = client or GeronimoCloudClient()
Initialize the cloud backend.
Arguments:
- project: Project name (optional context).
- version: Project version (optional context).
- namespace: Optional namespace for cross-user access.
- client: Optional pre-configured client.
94 def save(self, name: str, artifact: Any, metadata: dict) -> str: 95 """Save an artifact to the cloud. 96 97 Args: 98 name: Artifact name. 99 artifact: Python object to serialize. 100 metadata: Artifact metadata. 101 102 Returns: 103 S3 URI of the saved artifact. 104 105 Raises: 106 RuntimeError: If not authenticated. 107 httpx.HTTPError: If API request fails. 108 """ 109 self._check_auth() 110 111 # 1. Serialize artifact to get actual size 112 with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: 113 pickle.dump(artifact, f) 114 f.flush() 115 temp_path = f.name 116 size_bytes = os.path.getsize(temp_path) 117 118 try: 119 # 2. Get upload URL from cloud API 120 with api_client( 121 self.client.api_url, self.client.headers, operation=f"save/{name}" 122 ) as http: 123 logger.debug(f"Requesting upload URL for artifact '{name}'") 124 125 payload = { 126 "project": self.project, 127 "version": self.version, 128 "name": name, 129 "size_bytes": size_bytes, 130 "metadata": metadata, 131 } 132 if self.namespace: 133 payload["namespace"] = self.namespace 134 135 resp = http.post("/v1/artifacts/cloud-save", json=payload) 136 resp.raise_for_status() 137 data = resp.json() 138 upload_url = data["upload_url"] 139 artifact_id = data["id"] 140 s3_uri = data["s3_uri"] 141 142 logger.info(f"Uploading artifact '{name}' to cloud (ID: {artifact_id})") 143 144 # 3. Upload content to S3 145 with open(temp_path, "rb") as f: 146 with transfer_client(operation=f"upload/{name}") as http: 147 resp = http.put(upload_url, content=f) 148 resp.raise_for_status() 149 150 # 4. Confirm upload 151 with api_client( 152 self.client.api_url, self.client.headers, operation=f"confirm/{name}" 153 ) as http: 154 resp = http.post( 155 f"/v1/artifacts/{artifact_id}/confirm", 156 json={"size_bytes": size_bytes}, 157 ) 158 resp.raise_for_status() 159 160 logger.info(f"Successfully saved artifact '{name}' to {s3_uri}") 161 return s3_uri 162 163 finally: 164 if os.path.exists(temp_path): 165 os.unlink(temp_path)
Save an artifact to the cloud.
Arguments:
- name: Artifact name.
- artifact: Python object to serialize.
- metadata: Artifact metadata.
Returns:
S3 URI of the saved artifact.
Raises:
- RuntimeError: If not authenticated.
- httpx.HTTPError: If API request fails.
167 def load(self, uri: str) -> Any: 168 """Load an artifact from the cloud. 169 170 Args: 171 uri: S3 URI (s3://bucket/user/project/version/name) or artifact name. 172 173 Returns: 174 Deserialized artifact. 175 176 Raises: 177 ValueError: If URI cannot be parsed or context is missing. 178 RuntimeError: If not authenticated. 179 httpx.HTTPError: If API request fails. 180 """ 181 self._check_auth() 182 183 project, version, name = self._parse_artifact_uri(uri) 184 185 # 1. Get download URL 186 with api_client( 187 self.client.api_url, self.client.headers, operation=f"load/{name}" 188 ) as http: 189 logger.debug(f"Requesting download URL for '{project}/{version}/{name}'") 190 191 payload = { 192 "project": project, 193 "version": version, 194 "name": name, 195 } 196 if self.namespace: 197 payload["namespace"] = self.namespace 198 199 resp = http.post("/v1/artifacts/cloud-load", json=payload) 200 resp.raise_for_status() 201 download_url = resp.json()["download_url"] 202 203 # 2. Download and deserialize 204 logger.info(f"Loading artifact '{name}' from cloud") 205 with transfer_client(operation=f"download/{name}") as http: 206 resp = http.get(download_url) 207 resp.raise_for_status() 208 return pickle.loads(resp.content)
Load an artifact from the cloud.
Arguments:
- uri: S3 URI (s3://bucket/user/project/version/name) or artifact name.
Returns:
Deserialized artifact.
Raises:
- ValueError: If URI cannot be parsed or context is missing.
- RuntimeError: If not authenticated.
- httpx.HTTPError: If API request fails.
210 def list(self, prefix: Optional[str] = None) -> list[str]: 211 """List artifacts. 212 213 Args: 214 prefix: Optional name prefix to filter results. 215 216 Returns: 217 List of artifact S3 URIs. 218 219 Raises: 220 RuntimeError: If not authenticated. 221 httpx.HTTPError: If API request fails. 222 """ 223 self._check_auth() 224 225 params = {} 226 if self.project: 227 params["project"] = self.project 228 if self.version: 229 params["version"] = self.version 230 if self.namespace: 231 params["namespace"] = self.namespace 232 if prefix: 233 params["prefix"] = prefix 234 235 with api_client( 236 self.client.api_url, self.client.headers, operation="list" 237 ) as http: 238 logger.debug(f"Listing artifacts with params: {params}") 239 resp = http.get("/v1/artifacts", params=params) 240 resp.raise_for_status() 241 artifacts = resp.json().get("artifacts", []) 242 return [a["s3_uri"] for a in artifacts]
List artifacts.
Arguments:
- prefix: Optional name prefix to filter results.
Returns:
List of artifact S3 URIs.
Raises:
- RuntimeError: If not authenticated.
- httpx.HTTPError: If API request fails.
244 def delete(self, uri: str) -> None: 245 """Delete an artifact. 246 247 Args: 248 uri: S3 URI or artifact name. 249 250 Raises: 251 ValueError: If URI cannot be parsed or context is missing. 252 RuntimeError: If not authenticated. 253 httpx.HTTPError: If API request fails. 254 """ 255 self._check_auth() 256 257 project, version, name = self._parse_artifact_uri(uri) 258 259 with api_client( 260 self.client.api_url, self.client.headers, operation=f"delete/{name}" 261 ) as http: 262 # Find artifact ID by searching 263 search_params = { 264 "project": project, 265 "version": version, 266 "name": name 267 } 268 if self.namespace: 269 search_params["namespace"] = self.namespace 270 271 logger.debug(f"Searching for artifact to delete: {search_params}") 272 resp = http.get("/v1/artifacts", params=search_params) 273 resp.raise_for_status() 274 matches = resp.json().get("artifacts", []) 275 276 if not matches: 277 logger.warning(f"Artifact '{name}' not found, skipping delete") 278 return 279 280 # Delete all matches 281 for artifact in matches: 282 artifact_id = artifact["id"] 283 logger.info(f"Deleting artifact '{name}' (ID: {artifact_id})") 284 resp = http.delete(f"/v1/artifacts/{artifact_id}") 285 resp.raise_for_status()
Delete an artifact.
Arguments:
- uri: S3 URI or artifact name.
Raises:
- ValueError: If URI cannot be parsed or context is missing.
- RuntimeError: If not authenticated.
- httpx.HTTPError: If API request fails.