morphml 1.0.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.
Potentially problematic release.
This version of morphml might be problematic. Click here for more details.
- morphml/__init__.py +14 -0
- morphml/api/__init__.py +26 -0
- morphml/api/app.py +326 -0
- morphml/api/auth.py +193 -0
- morphml/api/client.py +338 -0
- morphml/api/models.py +132 -0
- morphml/api/rate_limit.py +192 -0
- morphml/benchmarking/__init__.py +36 -0
- morphml/benchmarking/comparison.py +430 -0
- morphml/benchmarks/__init__.py +56 -0
- morphml/benchmarks/comparator.py +409 -0
- morphml/benchmarks/datasets.py +280 -0
- morphml/benchmarks/metrics.py +199 -0
- morphml/benchmarks/openml_suite.py +201 -0
- morphml/benchmarks/problems.py +289 -0
- morphml/benchmarks/suite.py +318 -0
- morphml/cli/__init__.py +5 -0
- morphml/cli/commands/experiment.py +329 -0
- morphml/cli/main.py +457 -0
- morphml/cli/quickstart.py +312 -0
- morphml/config.py +278 -0
- morphml/constraints/__init__.py +19 -0
- morphml/constraints/handler.py +205 -0
- morphml/constraints/predicates.py +285 -0
- morphml/core/__init__.py +3 -0
- morphml/core/crossover.py +449 -0
- morphml/core/dsl/README.md +359 -0
- morphml/core/dsl/__init__.py +72 -0
- morphml/core/dsl/ast_nodes.py +364 -0
- morphml/core/dsl/compiler.py +318 -0
- morphml/core/dsl/layers.py +368 -0
- morphml/core/dsl/lexer.py +336 -0
- morphml/core/dsl/parser.py +455 -0
- morphml/core/dsl/search_space.py +386 -0
- morphml/core/dsl/syntax.py +199 -0
- morphml/core/dsl/type_system.py +361 -0
- morphml/core/dsl/validator.py +386 -0
- morphml/core/graph/__init__.py +40 -0
- morphml/core/graph/edge.py +124 -0
- morphml/core/graph/graph.py +507 -0
- morphml/core/graph/mutations.py +409 -0
- morphml/core/graph/node.py +196 -0
- morphml/core/graph/serialization.py +361 -0
- morphml/core/graph/visualization.py +431 -0
- morphml/core/objectives/__init__.py +20 -0
- morphml/core/search/__init__.py +33 -0
- morphml/core/search/individual.py +252 -0
- morphml/core/search/parameters.py +453 -0
- morphml/core/search/population.py +375 -0
- morphml/core/search/search_engine.py +340 -0
- morphml/distributed/__init__.py +76 -0
- morphml/distributed/fault_tolerance.py +497 -0
- morphml/distributed/health_monitor.py +348 -0
- morphml/distributed/master.py +709 -0
- morphml/distributed/proto/README.md +224 -0
- morphml/distributed/proto/__init__.py +74 -0
- morphml/distributed/proto/worker.proto +170 -0
- morphml/distributed/proto/worker_pb2.py +79 -0
- morphml/distributed/proto/worker_pb2_grpc.py +423 -0
- morphml/distributed/resource_manager.py +416 -0
- morphml/distributed/scheduler.py +567 -0
- morphml/distributed/storage/__init__.py +33 -0
- morphml/distributed/storage/artifacts.py +381 -0
- morphml/distributed/storage/cache.py +366 -0
- morphml/distributed/storage/checkpointing.py +329 -0
- morphml/distributed/storage/database.py +459 -0
- morphml/distributed/worker.py +549 -0
- morphml/evaluation/__init__.py +5 -0
- morphml/evaluation/heuristic.py +237 -0
- morphml/exceptions.py +55 -0
- morphml/execution/__init__.py +5 -0
- morphml/execution/local_executor.py +350 -0
- morphml/integrations/__init__.py +28 -0
- morphml/integrations/jax_adapter.py +206 -0
- morphml/integrations/pytorch_adapter.py +530 -0
- morphml/integrations/sklearn_adapter.py +206 -0
- morphml/integrations/tensorflow_adapter.py +230 -0
- morphml/logging_config.py +93 -0
- morphml/meta_learning/__init__.py +66 -0
- morphml/meta_learning/architecture_similarity.py +277 -0
- morphml/meta_learning/experiment_database.py +240 -0
- morphml/meta_learning/knowledge_base/__init__.py +19 -0
- morphml/meta_learning/knowledge_base/embedder.py +179 -0
- morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
- morphml/meta_learning/knowledge_base/meta_features.py +265 -0
- morphml/meta_learning/knowledge_base/vector_store.py +271 -0
- morphml/meta_learning/predictors/__init__.py +27 -0
- morphml/meta_learning/predictors/ensemble.py +221 -0
- morphml/meta_learning/predictors/gnn_predictor.py +552 -0
- morphml/meta_learning/predictors/learning_curve.py +231 -0
- morphml/meta_learning/predictors/proxy_metrics.py +261 -0
- morphml/meta_learning/strategy_evolution/__init__.py +27 -0
- morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
- morphml/meta_learning/strategy_evolution/bandit.py +276 -0
- morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
- morphml/meta_learning/transfer.py +581 -0
- morphml/meta_learning/warm_start.py +286 -0
- morphml/optimizers/__init__.py +74 -0
- morphml/optimizers/adaptive_operators.py +399 -0
- morphml/optimizers/bayesian/__init__.py +52 -0
- morphml/optimizers/bayesian/acquisition.py +387 -0
- morphml/optimizers/bayesian/base.py +319 -0
- morphml/optimizers/bayesian/gaussian_process.py +635 -0
- morphml/optimizers/bayesian/smac.py +534 -0
- morphml/optimizers/bayesian/tpe.py +411 -0
- morphml/optimizers/differential_evolution.py +220 -0
- morphml/optimizers/evolutionary/__init__.py +61 -0
- morphml/optimizers/evolutionary/cma_es.py +416 -0
- morphml/optimizers/evolutionary/differential_evolution.py +556 -0
- morphml/optimizers/evolutionary/encoding.py +426 -0
- morphml/optimizers/evolutionary/particle_swarm.py +449 -0
- morphml/optimizers/genetic_algorithm.py +486 -0
- morphml/optimizers/gradient_based/__init__.py +22 -0
- morphml/optimizers/gradient_based/darts.py +550 -0
- morphml/optimizers/gradient_based/enas.py +585 -0
- morphml/optimizers/gradient_based/operations.py +474 -0
- morphml/optimizers/gradient_based/utils.py +601 -0
- morphml/optimizers/hill_climbing.py +169 -0
- morphml/optimizers/multi_objective/__init__.py +56 -0
- morphml/optimizers/multi_objective/indicators.py +504 -0
- morphml/optimizers/multi_objective/nsga2.py +647 -0
- morphml/optimizers/multi_objective/visualization.py +427 -0
- morphml/optimizers/nsga2.py +308 -0
- morphml/optimizers/random_search.py +172 -0
- morphml/optimizers/simulated_annealing.py +181 -0
- morphml/plugins/__init__.py +35 -0
- morphml/plugins/custom_evaluator_example.py +81 -0
- morphml/plugins/custom_optimizer_example.py +63 -0
- morphml/plugins/plugin_system.py +454 -0
- morphml/reports/__init__.py +30 -0
- morphml/reports/generator.py +362 -0
- morphml/tracking/__init__.py +7 -0
- morphml/tracking/experiment.py +309 -0
- morphml/tracking/logger.py +301 -0
- morphml/tracking/reporter.py +357 -0
- morphml/utils/__init__.py +6 -0
- morphml/utils/checkpoint.py +189 -0
- morphml/utils/comparison.py +390 -0
- morphml/utils/export.py +407 -0
- morphml/utils/progress.py +392 -0
- morphml/utils/validation.py +392 -0
- morphml/version.py +7 -0
- morphml/visualization/__init__.py +50 -0
- morphml/visualization/analytics.py +423 -0
- morphml/visualization/architecture_diagrams.py +353 -0
- morphml/visualization/architecture_plot.py +223 -0
- morphml/visualization/convergence_plot.py +174 -0
- morphml/visualization/crossover_viz.py +386 -0
- morphml/visualization/graph_viz.py +338 -0
- morphml/visualization/pareto_plot.py +149 -0
- morphml/visualization/plotly_dashboards.py +422 -0
- morphml/visualization/population.py +309 -0
- morphml/visualization/progress.py +260 -0
- morphml-1.0.0.dist-info/METADATA +434 -0
- morphml-1.0.0.dist-info/RECORD +158 -0
- morphml-1.0.0.dist-info/WHEEL +4 -0
- morphml-1.0.0.dist-info/entry_points.txt +3 -0
- morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""S3/MinIO artifact storage for models and checkpoints.
|
|
2
|
+
|
|
3
|
+
Provides object storage for large binary artifacts.
|
|
4
|
+
|
|
5
|
+
Author: Eshan Roy <eshanized@proton.me>
|
|
6
|
+
Organization: TONMOY INFRASTRUCTURE & VISION
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import boto3
|
|
15
|
+
from botocore.exceptions import ClientError
|
|
16
|
+
|
|
17
|
+
BOTO3_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
BOTO3_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
from morphml.exceptions import DistributedError
|
|
22
|
+
from morphml.logging_config import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArtifactStore:
|
|
28
|
+
"""
|
|
29
|
+
S3-compatible artifact storage.
|
|
30
|
+
|
|
31
|
+
Stores large binary artifacts:
|
|
32
|
+
- Trained model weights (.pt, .h5, .ckpt)
|
|
33
|
+
- Architecture checkpoints
|
|
34
|
+
- Visualization plots (.png, .html)
|
|
35
|
+
- Training logs (.txt, .json)
|
|
36
|
+
|
|
37
|
+
Compatible with both AWS S3 and MinIO.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
bucket: S3 bucket name
|
|
41
|
+
endpoint_url: Custom endpoint for MinIO (None for AWS S3)
|
|
42
|
+
aws_access_key: AWS access key ID (or from environment)
|
|
43
|
+
aws_secret_key: AWS secret access key (or from environment)
|
|
44
|
+
region: AWS region (default: us-east-1)
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> # AWS S3
|
|
48
|
+
>>> store = ArtifactStore(bucket='morphml-artifacts')
|
|
49
|
+
>>>
|
|
50
|
+
>>> # MinIO
|
|
51
|
+
>>> store = ArtifactStore(
|
|
52
|
+
... bucket='morphml',
|
|
53
|
+
... endpoint_url='http://localhost:9000',
|
|
54
|
+
... aws_access_key='minioadmin',
|
|
55
|
+
... aws_secret_key='minioadmin'
|
|
56
|
+
... )
|
|
57
|
+
>>>
|
|
58
|
+
>>> # Upload
|
|
59
|
+
>>> store.upload_file('model.pt', 'experiments/exp1/model.pt')
|
|
60
|
+
>>>
|
|
61
|
+
>>> # Download
|
|
62
|
+
>>> store.download_file('experiments/exp1/model.pt', 'model.pt')
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
bucket: str,
|
|
68
|
+
endpoint_url: Optional[str] = None,
|
|
69
|
+
aws_access_key: Optional[str] = None,
|
|
70
|
+
aws_secret_key: Optional[str] = None,
|
|
71
|
+
region: str = "us-east-1",
|
|
72
|
+
):
|
|
73
|
+
"""Initialize artifact store."""
|
|
74
|
+
if not BOTO3_AVAILABLE:
|
|
75
|
+
raise DistributedError("boto3 not available. Install with: pip install boto3")
|
|
76
|
+
|
|
77
|
+
self.bucket = bucket
|
|
78
|
+
self.endpoint_url = endpoint_url
|
|
79
|
+
|
|
80
|
+
# Create S3 client
|
|
81
|
+
self.s3 = boto3.client(
|
|
82
|
+
"s3",
|
|
83
|
+
endpoint_url=endpoint_url,
|
|
84
|
+
aws_access_key_id=aws_access_key or os.getenv("AWS_ACCESS_KEY_ID"),
|
|
85
|
+
aws_secret_access_key=aws_secret_key or os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
86
|
+
region_name=region,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create bucket if not exists
|
|
90
|
+
self._ensure_bucket_exists()
|
|
91
|
+
|
|
92
|
+
logger.info(f"Initialized artifact store: {bucket} (endpoint: {endpoint_url or 'AWS S3'})")
|
|
93
|
+
|
|
94
|
+
def _ensure_bucket_exists(self) -> None:
|
|
95
|
+
"""Create bucket if it doesn't exist."""
|
|
96
|
+
try:
|
|
97
|
+
self.s3.head_bucket(Bucket=self.bucket)
|
|
98
|
+
logger.debug(f"Bucket {self.bucket} exists")
|
|
99
|
+
except ClientError as e:
|
|
100
|
+
error_code = e.response.get("Error", {}).get("Code")
|
|
101
|
+
|
|
102
|
+
if error_code == "404":
|
|
103
|
+
# Bucket doesn't exist, create it
|
|
104
|
+
try:
|
|
105
|
+
self.s3.create_bucket(Bucket=self.bucket)
|
|
106
|
+
logger.info(f"Created bucket: {self.bucket}")
|
|
107
|
+
except ClientError as create_error:
|
|
108
|
+
raise DistributedError(f"Failed to create bucket {self.bucket}: {create_error}")
|
|
109
|
+
else:
|
|
110
|
+
raise DistributedError(f"Failed to access bucket {self.bucket}: {e}")
|
|
111
|
+
|
|
112
|
+
def upload_file(
|
|
113
|
+
self, local_path: str, s3_key: str, metadata: Optional[Dict[str, str]] = None
|
|
114
|
+
) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Upload file to S3.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
local_path: Local file path
|
|
120
|
+
s3_key: S3 object key (path in bucket)
|
|
121
|
+
metadata: Optional metadata tags
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
extra_args = {}
|
|
125
|
+
if metadata:
|
|
126
|
+
extra_args["Metadata"] = metadata
|
|
127
|
+
|
|
128
|
+
self.s3.upload_file(local_path, self.bucket, s3_key, ExtraArgs=extra_args)
|
|
129
|
+
|
|
130
|
+
logger.info(f"Uploaded {local_path} to s3://{self.bucket}/{s3_key}")
|
|
131
|
+
|
|
132
|
+
except ClientError as e:
|
|
133
|
+
raise DistributedError(f"Failed to upload {local_path}: {e}")
|
|
134
|
+
|
|
135
|
+
def download_file(self, s3_key: str, local_path: str) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Download file from S3.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
s3_key: S3 object key
|
|
141
|
+
local_path: Local destination path
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
# Create parent directories
|
|
145
|
+
Path(local_path).parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
self.s3.download_file(self.bucket, s3_key, local_path)
|
|
148
|
+
|
|
149
|
+
logger.info(f"Downloaded s3://{self.bucket}/{s3_key} to {local_path}")
|
|
150
|
+
|
|
151
|
+
except ClientError as e:
|
|
152
|
+
raise DistributedError(f"Failed to download {s3_key}: {e}")
|
|
153
|
+
|
|
154
|
+
def upload_bytes(
|
|
155
|
+
self, data: bytes, s3_key: str, metadata: Optional[Dict[str, str]] = None
|
|
156
|
+
) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Upload bytes directly to S3.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
data: Bytes to upload
|
|
162
|
+
s3_key: S3 object key
|
|
163
|
+
metadata: Optional metadata
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
extra_args = {}
|
|
167
|
+
if metadata:
|
|
168
|
+
extra_args["Metadata"] = metadata
|
|
169
|
+
|
|
170
|
+
self.s3.put_object(Bucket=self.bucket, Key=s3_key, Body=data, **extra_args)
|
|
171
|
+
|
|
172
|
+
logger.info(f"Uploaded {len(data)} bytes to s3://{self.bucket}/{s3_key}")
|
|
173
|
+
|
|
174
|
+
except ClientError as e:
|
|
175
|
+
raise DistributedError(f"Failed to upload bytes to {s3_key}: {e}")
|
|
176
|
+
|
|
177
|
+
def download_bytes(self, s3_key: str) -> bytes:
|
|
178
|
+
"""
|
|
179
|
+
Download bytes from S3.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
s3_key: S3 object key
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Downloaded bytes
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
response = self.s3.get_object(Bucket=self.bucket, Key=s3_key)
|
|
189
|
+
data = response["Body"].read()
|
|
190
|
+
|
|
191
|
+
logger.info(f"Downloaded {len(data)} bytes from s3://{self.bucket}/{s3_key}")
|
|
192
|
+
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
except ClientError as e:
|
|
196
|
+
raise DistributedError(f"Failed to download {s3_key}: {e}")
|
|
197
|
+
|
|
198
|
+
def exists(self, s3_key: str) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Check if object exists.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
s3_key: S3 object key
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if object exists
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
self.s3.head_object(Bucket=self.bucket, Key=s3_key)
|
|
210
|
+
return True
|
|
211
|
+
except ClientError:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def delete(self, s3_key: str) -> None:
|
|
215
|
+
"""
|
|
216
|
+
Delete object.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
s3_key: S3 object key
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
self.s3.delete_object(Bucket=self.bucket, Key=s3_key)
|
|
223
|
+
logger.info(f"Deleted s3://{self.bucket}/{s3_key}")
|
|
224
|
+
except ClientError as e:
|
|
225
|
+
raise DistributedError(f"Failed to delete {s3_key}: {e}")
|
|
226
|
+
|
|
227
|
+
def list_objects(self, prefix: str = "", max_keys: int = 1000) -> List[Dict[str, Any]]:
|
|
228
|
+
"""
|
|
229
|
+
List objects with prefix.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
prefix: Key prefix to filter
|
|
233
|
+
max_keys: Maximum number of keys to return
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of object metadata dictionaries
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
response = self.s3.list_objects_v2(Bucket=self.bucket, Prefix=prefix, MaxKeys=max_keys)
|
|
240
|
+
|
|
241
|
+
if "Contents" not in response:
|
|
242
|
+
return []
|
|
243
|
+
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
"key": obj["Key"],
|
|
247
|
+
"size": obj["Size"],
|
|
248
|
+
"last_modified": obj["LastModified"],
|
|
249
|
+
}
|
|
250
|
+
for obj in response["Contents"]
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
except ClientError as e:
|
|
254
|
+
raise DistributedError(f"Failed to list objects: {e}")
|
|
255
|
+
|
|
256
|
+
def list_keys(self, prefix: str = "") -> List[str]:
|
|
257
|
+
"""
|
|
258
|
+
List object keys with prefix.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
prefix: Key prefix to filter
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
List of object keys
|
|
265
|
+
"""
|
|
266
|
+
objects = self.list_objects(prefix)
|
|
267
|
+
return [obj["key"] for obj in objects]
|
|
268
|
+
|
|
269
|
+
def get_metadata(self, s3_key: str) -> Dict[str, Any]:
|
|
270
|
+
"""
|
|
271
|
+
Get object metadata.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
s3_key: S3 object key
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Metadata dictionary
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
response = self.s3.head_object(Bucket=self.bucket, Key=s3_key)
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"size": response["ContentLength"],
|
|
284
|
+
"last_modified": response["LastModified"],
|
|
285
|
+
"content_type": response.get("ContentType"),
|
|
286
|
+
"metadata": response.get("Metadata", {}),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
except ClientError as e:
|
|
290
|
+
raise DistributedError(f"Failed to get metadata for {s3_key}: {e}")
|
|
291
|
+
|
|
292
|
+
def get_presigned_url(
|
|
293
|
+
self, s3_key: str, expiration: int = 3600, method: str = "get_object"
|
|
294
|
+
) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Generate presigned URL for temporary access.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
s3_key: S3 object key
|
|
300
|
+
expiration: URL expiration in seconds
|
|
301
|
+
method: S3 method ('get_object' or 'put_object')
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Presigned URL
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
url = self.s3.generate_presigned_url(
|
|
308
|
+
method,
|
|
309
|
+
Params={"Bucket": self.bucket, "Key": s3_key},
|
|
310
|
+
ExpiresIn=expiration,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return url
|
|
314
|
+
|
|
315
|
+
except ClientError as e:
|
|
316
|
+
raise DistributedError(f"Failed to generate presigned URL: {e}")
|
|
317
|
+
|
|
318
|
+
def copy(self, source_key: str, dest_key: str) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Copy object within bucket.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
source_key: Source object key
|
|
324
|
+
dest_key: Destination object key
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
copy_source = {"Bucket": self.bucket, "Key": source_key}
|
|
328
|
+
self.s3.copy_object(CopySource=copy_source, Bucket=self.bucket, Key=dest_key)
|
|
329
|
+
|
|
330
|
+
logger.info(f"Copied {source_key} to {dest_key}")
|
|
331
|
+
|
|
332
|
+
except ClientError as e:
|
|
333
|
+
raise DistributedError(f"Failed to copy {source_key}: {e}")
|
|
334
|
+
|
|
335
|
+
def delete_prefix(self, prefix: str) -> int:
|
|
336
|
+
"""
|
|
337
|
+
Delete all objects with prefix.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
prefix: Key prefix
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Number of objects deleted
|
|
344
|
+
"""
|
|
345
|
+
keys = self.list_keys(prefix)
|
|
346
|
+
|
|
347
|
+
if not keys:
|
|
348
|
+
return 0
|
|
349
|
+
|
|
350
|
+
# Delete in batches of 1000 (S3 limit)
|
|
351
|
+
deleted = 0
|
|
352
|
+
for i in range(0, len(keys), 1000):
|
|
353
|
+
batch = keys[i : i + 1000]
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
self.s3.delete_objects(
|
|
357
|
+
Bucket=self.bucket,
|
|
358
|
+
Delete={"Objects": [{"Key": key} for key in batch]},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
deleted += len(batch)
|
|
362
|
+
|
|
363
|
+
except ClientError as e:
|
|
364
|
+
logger.error(f"Failed to delete batch: {e}")
|
|
365
|
+
|
|
366
|
+
logger.info(f"Deleted {deleted} objects with prefix {prefix}")
|
|
367
|
+
|
|
368
|
+
return deleted
|
|
369
|
+
|
|
370
|
+
def get_total_size(self, prefix: str = "") -> int:
|
|
371
|
+
"""
|
|
372
|
+
Get total size of objects with prefix.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
prefix: Key prefix
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Total size in bytes
|
|
379
|
+
"""
|
|
380
|
+
objects = self.list_objects(prefix)
|
|
381
|
+
return sum(obj["size"] for obj in objects)
|