aipmodel 0.2.67__tar.gz → 0.2.69__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipmodel
3
- Version: 0.2.67
3
+ Version: 0.2.69
4
4
  Summary: SDK for model registration, versioning, and storage
5
5
  Home-page: https://github.com/AIP-MLOPS/model-registry
6
6
  Author: AIP MLOPS Team
@@ -1,6 +1,6 @@
1
1
  # AIP Model SDK
2
2
 
3
- This SDK provides a robust interface for registering, uploading, downloading, listing, and deleting machine learning models using ClearML and S3 (Ceph) as storage. It supports **automatic versioning**, **categorization**, **tagging**, **metadata transparency**, **web link generation**, and fail-fast connectivity checks.
3
+ This SDK provides a robust interface for registering, uploading, downloading, listing, and deleting machine learning models using Mlops-Core and S3 (Ceph) as storage. It supports **automatic versioning**, **categorization**, **tagging**, **metadata transparency**, **web link generation**, and fail-fast connectivity checks.
4
4
 
5
5
  ---
6
6
 
@@ -20,11 +20,13 @@ pip install aipmodel
20
20
  To use the SDK, you must set up your environment variables. You can create a `.env` file in your project root:
21
21
 
22
22
  ```env
23
- # ClearML API Credentials
24
- CLEARML_API_HOST="http://your-clearml-server:8008"
25
- CLEARML_WEB_HOST="http://your-clearml-web-ui:8080" # Required for generating model web links
26
- CLEARML_ACCESS_KEY="your_access_key"
27
- CLEARML_SECRET_KEY="your_secret_key"
23
+ # Mlops-Core API Credentials
24
+ # (The legacy CLEARML_API_HOST / CLEARML_WEB_HOST env var names and constructor
25
+ # kwargs are still accepted as a deprecated fallback for backward compatibility.)
26
+ MLOPS_CORE_API_HOST="http://your-mlops-core-server:8008"
27
+ MLOPS_CORE_WEB_HOST="http://your-mlops-core-web-ui:8080" # Required for generating model web links
28
+ MLOPS_CORE_ACCESS_KEY="your_access_key"
29
+ MLOPS_CORE_SECRET_KEY="your_secret_key"
28
30
 
29
31
  # Ceph / S3 Storage
30
32
  CEPH_ENDPOINT_URL="https://your-ceph-endpoint.com"
@@ -59,7 +61,7 @@ S3_BOTO_MAX_ATTEMPTS="10" # botocore request-level retry attempts (adapti
59
61
 
60
62
  ```
61
63
 
62
- > **Note:** `CLEARML_WEB_HOST` is new. If not provided, the SDK will default to the API host, which might result in incorrect clickable links in the output.
64
+ > **Note:** `MLOPS_CORE_WEB_HOST` is new. If not provided, the SDK will default to the API host, which might result in incorrect clickable links in the output. The legacy `CLEARML_API_HOST` / `CLEARML_WEB_HOST` names still work but are deprecated; when both old and new names are set, the `MLOPS_CORE_*` name wins.
63
65
 
64
66
  > Every constant the SDK relies on (timeouts, categories, transfer tuning, ACL) is overridable via `.env` — nothing important is hardcoded.
65
67
 
@@ -97,7 +99,7 @@ reset). The SDK now **retries these automatically**, so `add_model`,
97
99
 
98
100
  `add_model` is **concurrency-safe** — you can run many adds at the same time:
99
101
 
100
- - Adds with **different model names** run **fully in parallel** (own ClearML id,
102
+ - Adds with **different model names** run **fully in parallel** (own Mlops-Core id,
101
103
  own S3 prefix — no interference).
102
104
  - Adds with the **same `(project, model_name)`** are **serialized** by a
103
105
  process-wide lock. Otherwise two simultaneous same-name adds would race on the
@@ -116,7 +118,7 @@ reset). The SDK now **retries these automatically**, so `add_model`,
116
118
  automatically on the next add to that model.
117
119
  - The lock is **per-process**. One server process (e.g. a single `uvicorn`
118
120
  worker) is fully protected; for multiple workers/replicas add a distributed
119
- lock (ClearML/Redis).
121
+ lock (Mlops-Core/Redis).
120
122
 
121
123
  ## Access Control (ACL)
122
124
 
@@ -143,7 +145,7 @@ Requires `ACL_ENABLED=true` and that you have `admin`-level access on the model.
143
145
 
144
146
  ## Cross-User Visibility (Public Models)
145
147
 
146
- Every model lives in its owner's own ClearML project (`project_<owner_username>`) — that part hasn't changed. What changed is **how `list_models`/`get_model_id_by_name`/`get_model_info`/`get_model` look things up**:
148
+ Every model lives in its owner's own Mlops-Core project (`project_<owner_username>`) — that part hasn't changed. What changed is **how `list_models`/`get_model_id_by_name`/`get_model_info`/`get_model` look things up**:
147
149
 
148
150
  - **`list_models()`** (default `include_public=True`): returns your own models (private + public) **plus** every other user's model that is marked `public=True`. Other users' **private** models are never returned, under any setting. Each entry's `owner` field is resolved from the model's actual project, so models from other users show their real owner, not yours.
149
151
  - Pass `include_public=False` to get the exact pre-existing behavior: your own project only, nothing from other users.
@@ -152,7 +154,7 @@ Every model lives in its owner's own ClearML project (`project_<owner_username>`
152
154
  - **`get_model_link`** now resolves the link using the model's *actual* owning project (previously it always used your own project ID, which silently produced a wrong link for any model you didn't own — this never came up before because nothing could return another user's model anyway).
153
155
  - Write operations (`add_model`, `delete_model`, `set_model_public`, `set_latest_version`) are **unchanged** — they still only ever operate within your own project by name. To act on a model you don't own (e.g. as an ACL-granted admin), pass its `model_id` directly (already supported) rather than its name.
154
156
 
155
- **Performance note:** when `include_public=True` (the default), `list_models()` makes one extra unscoped ClearML query (`models.get_all` with no project filter) to find other users' public models — on a very large ClearML deployment this returns every model company-wide before filtering down to public ones. If that ever becomes slow, set `include_public=False` and only fetch other users' models on demand via `owner=`.
157
+ **Performance note:** when `include_public=True` (the default), `list_models()` makes one extra unscoped Mlops-Core query (`models.get_all` with no project filter) to find other users' public models — on a very large Mlops-Core deployment this returns every model company-wide before filtering down to public ones. If that ever becomes slow, set `include_public=False` and only fetch other users' models on demand via `owner=`.
156
158
 
157
159
  **Backward compatibility:** `get_model_id_by_name`, `get_model`, `get_model_info` keep their exact old signatures plus one new optional trailing argument — no existing call site breaks. `list_models()`'s default *output* now includes more rows than before (other users' public models) since that was the explicit point of this change; pass `include_public=False` if your code depends on the exact old row set.
158
160
 
@@ -174,8 +176,8 @@ print("\n[STEP 1] Initialize MLOps Manager")
174
176
  # verbose=True enables detailed logging and transparency mode.
175
177
  manager = MLOpsManager(
176
178
  user_token=os.getenv("USER_TOKEN"),
177
- CLEARML_API_HOST=os.getenv("CLEARML_API_HOST"),
178
- CLEARML_WEB_HOST=os.getenv("CLEARML_WEB_HOST"), # For generating UI links
179
+ MLOPS_CORE_API_HOST=os.getenv("MLOPS_CORE_API_HOST"),
180
+ MLOPS_CORE_WEB_HOST=os.getenv("MLOPS_CORE_WEB_HOST"), # For generating UI links
179
181
  CEPH_ENDPOINT_URL=os.getenv("CEPH_ENDPOINT_URL"),
180
182
  USER_MANAGEMENT_API=os.getenv("USER_MANAGEMENT_API"),
181
183
  verbose=True
@@ -234,7 +236,7 @@ print(f" -> Registered ID: {hf_model_id}")
234
236
 
235
237
  # --- STEP 6: Get Detailed Info ---
236
238
  print("\n[STEP 6] Get Detailed Model Info")
237
- # Displays ID, Public status, Total Versions, Size, and ClearML Web Link
239
+ # Displays ID, Public status, Total Versions, Size, and Mlops-Core Web Link
238
240
  manager.get_model_info("demo_model_local")
239
241
 
240
242
  # --- STEP 7: List All Models ---
@@ -287,7 +289,7 @@ print("\n[STEP 10] Deletion")
287
289
  # Delete a specific version (Removes files + Metadata entry)
288
290
  manager.delete_model(model_name="demo_model_local", version="v1.0")
289
291
 
290
- # Delete the entire model (Removes all versions + ClearML ID)
292
+ # Delete the entire model (Removes all versions + Mlops-Core ID)
291
293
  manager.delete_model(model_name="demo_model_hf")
292
294
 
293
295
  ```
@@ -300,14 +302,14 @@ manager.delete_model(model_name="demo_model_hf")
300
302
 
301
303
  | Function | Input Arguments | Description |
302
304
  | --- | --- | --- |
303
- | **`__init__`** | `user_token`, `CLEARML_API_HOST`, `CLEARML_WEB_HOST`, `CEPH_ENDPOINT_URL`, `USER_MANAGEMENT_API`, `verbose`, `ACL_API_HOST`, `ACL_ENABLED` | Initializes connections. Performs fail-fast health checks. `ACL_*` args (or env vars) toggle ACL enforcement (default: disabled). |
305
+ | **`__init__`** | `user_token`, `MLOPS_CORE_API_HOST`, `MLOPS_CORE_WEB_HOST`, `CEPH_ENDPOINT_URL`, `USER_MANAGEMENT_API`, `verbose`, `ACL_API_HOST`, `ACL_ENABLED` | Initializes connections. Performs fail-fast health checks. `ACL_*` args (or env vars) toggle ACL enforcement (default: disabled). The legacy `CLEARML_API_HOST` / `CLEARML_WEB_HOST` kwargs are still accepted as deprecated aliases (the `MLOPS_CORE_*` kwargs win if both are given). |
304
306
  | **`add_model`** | `source_type`, `model_name`, `version`, `source_path`, `code_path`, `category`, `tags`, `external_ceph_*`, `model_folder_name` | Uploads a model. `version`: if `None`, generates `_v{n}` automatically. `category`: must be one of `get_allowed_categories()` (defaults to "Text Generation"/"Image Classification", overridable via `MODEL_ALLOWED_CATEGORIES`). `tags`: list of strings. `model_folder_name`: optional explicit folder name for the uploaded model; if omitted, it's derived automatically as before (backward compatible). |
305
307
  | **`get_model`** | `model_name`, `local_dest`, `version`, `owner` | Downloads model files. If `version` is omitted, downloads the version marked as 'latest'. `owner` (optional): resolve `model_name` from another user's project, e.g. to download a public model that isn't yours. |
306
308
  | **`get_model_info`** | `identifier` (Name or ID), `owner` | Prints detailed info including **Owner**, **Web Link**, Tags, Category, and Version History. `owner` (optional): resolve a name-based `identifier` from another user's project. |
307
309
  | **`get_allowed_categories`** | *(none)* | Returns the list of valid `category` values for `add_model`. |
308
310
  | **`list_models`** | `verbose`, `category`, `tag`, `public_only`, `private_only`, `include_public` | Lists your own models **plus** (by default, `include_public=True`) every other user's `public=True` model — never another user's private model. Each entry includes a real `owner` field. `verbose=True` shows full JSON metadata. `category`/`tag` filter by exact match; `public_only`/`private_only` filter by visibility, applied across the combined set. `include_public=False` restores the pre-existing own-project-only behavior. When ACL is enabled, results are additionally filtered to models you can access (plus public ones). See "Cross-User Visibility" above. |
309
- | **`get_model_id_by_name`** | `name`, `owner` | Resolves a model name to its ClearML ID. `owner` (optional): look it up in another user's project instead of your own. |
310
- | **`get_model_name_by_id`** | `model_id` | Resolves a ClearML ID back to its model name (works for any model, regardless of owner). |
311
+ | **`get_model_id_by_name`** | `name`, `owner` | Resolves a model name to its Mlops-Core ID. `owner` (optional): look it up in another user's project instead of your own. |
312
+ | **`get_model_name_by_id`** | `model_id` | Resolves a Mlops-Core ID back to its model name (works for any model, regardless of owner). |
311
313
  | **`get_latest_version`** | `model_identifier` (Name or ID) | Returns the version tag currently marked 'latest'. |
312
314
  | **`set_latest_version`** | `model_name`, `version` | Updates the 'latest' pointer to a specific version without modifying files. |
313
315
  | **`set_model_public`** | `model_name`, `public` | Moves a model's files between the private and public buckets (transactional, parallel server-side copy). |
@@ -348,12 +350,12 @@ The short version: run `uvicorn main:app --host 0.0.0.0 --port 8080`, pass `Auth
348
350
 
349
351
  ## Testing
350
352
 
351
- - `test_model_registry.py` / `test_ceph_s3_manager.py`: automated unit tests (mock the ClearML/S3 boundary, no live infra needed). Run with:
353
+ - `test_model_registry.py` / `test_ceph_s3_manager.py`: automated unit tests (mock the Mlops-Core/S3 boundary, no live infra needed). Run with:
352
354
  ```bash
353
355
  pip install pytest
354
356
  pytest test_model_registry.py test_ceph_s3_manager.py -v
355
357
  ```
356
- - `test.py`: a manual/live-integration demo script covering every scenario above against **real** ClearML/Ceph/ACL services — everything is commented out by default; uncomment only what you intend to run.
358
+ - `test.py`: a manual/live-integration demo script covering every scenario above against **real** Mlops-Core/Ceph/ACL services — everything is commented out by default; uncomment only what you intend to run.
357
359
 
358
360
  ---
359
361
 
@@ -858,7 +858,9 @@ class CephS3Manager:
858
858
  with tqdm(total=len(keys), desc="Moving", unit="file", ascii=True, disable=not self.verbose) as pbar:
859
859
  for src in keys:
860
860
  dest = src.replace(src_prefix, dest_prefix, 1)
861
- self.s3.copy_object(Bucket=self.bucket_name, CopySource={"Bucket": self.bucket_name, "Key": src}, Key=dest)
861
+ # s3.copy (not copy_object) handles files >5GB via
862
+ # automatic multipart copy, same as server_side_copy().
863
+ self.s3.copy({"Bucket": self.bucket_name, "Key": src}, self.bucket_name, dest, Config=self.transfer_config)
862
864
  pbar.update(1)
863
865
 
864
866
  self.delete_folder(src_prefix)
@@ -0,0 +1,15 @@
1
+ import os as _os
2
+
3
+ from .CephS3Manager import CephS3Manager
4
+ from .model_registry import MLOpsManager
5
+ from .update_checker import check_latest_version
6
+
7
+ # The PyPI update check makes a network call; doing that on EVERY import stalls
8
+ # air-gapped/cluster startups. Now opt-in: set AIPMODEL_UPDATE_CHECK=1 to enable.
9
+ if _os.environ.get("AIPMODEL_UPDATE_CHECK", "").lower() in ("1", "true", "yes"):
10
+ check_latest_version("aipmodel")
11
+
12
+ __all__ = ["MLOpsManager", "CephS3Manager"]
13
+
14
+ __version__ = "0.2.69"
15
+ __description__ = "SDK for model registration, versioning, and storage"
@@ -0,0 +1,23 @@
1
+ """
2
+ Typed SDK exceptions.
3
+
4
+ All subclass ValueError so existing `except ValueError` callers keep working
5
+ (fully backward compatible), while new callers — e.g. the HTTP API — can map
6
+ each type to a precise status code instead of guessing from one generic error.
7
+ """
8
+
9
+
10
+ class ModelRegistryError(ValueError):
11
+ """Base class for all aipmodel errors."""
12
+
13
+
14
+ class ModelNotFoundError(ModelRegistryError):
15
+ """The requested model / version does not exist."""
16
+
17
+
18
+ class MetadataError(ModelRegistryError):
19
+ """The registry (Mlops-Core) rejected the request or returned bad data."""
20
+
21
+
22
+ class StorageError(ModelRegistryError):
23
+ """S3 / Ceph operation failed (upload, download, copy, delete)."""
@@ -18,6 +18,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
18
18
 
19
19
  from .CephS3Manager import CephS3Manager
20
20
  from .acl_manager import AclManager
21
+ from .exceptions import ModelRegistryError, ModelNotFoundError, MetadataError, StorageError
21
22
 
22
23
  load_dotenv()
23
24
 
@@ -57,7 +58,7 @@ class ProjectsAPI:
57
58
 
58
59
  response = self._post("/projects.create", {"name": name, "description": description})
59
60
  if not response or "id" not in response:
60
- error_msg = "Failed to create project in ClearML"
61
+ error_msg = "Failed to create project in Mlops-Core"
61
62
  print(f"[SDK_FAIL] {error_msg}")
62
63
  raise ValueError(error_msg)
63
64
 
@@ -71,7 +72,7 @@ class ProjectsAPI:
71
72
 
72
73
  response = self._post("/projects.get_all")
73
74
  if not response or "projects" not in response:
74
- error_msg = "Failed to retrieve projects from ClearML"
75
+ error_msg = "Failed to retrieve projects from Mlops-Core"
75
76
  print(f"[SDK_FAIL] {error_msg}")
76
77
  raise ValueError(error_msg)
77
78
 
@@ -107,7 +108,7 @@ class ModelsAPI:
107
108
 
108
109
  error_msg = f"'models' not found in response"
109
110
  print(f"[SDK_ERROR] {error_msg}")
110
- raise ValueError("Failed to retrieve models from ClearML")
111
+ raise ValueError("Failed to retrieve models from Mlops-Core")
111
112
 
112
113
  def create(self, name, project_id, metadata=None, uri="", tags=None):
113
114
  if self.verbose:
@@ -126,7 +127,7 @@ class ModelsAPI:
126
127
 
127
128
  response = self._post("/models.create", payload)
128
129
  if not response or "id" not in response:
129
- error_msg = "Failed to create model in ClearML"
130
+ error_msg = "Failed to create model in Mlops-Core"
130
131
  print(f"[SDK_FAIL] {error_msg}")
131
132
  raise ValueError(error_msg)
132
133
 
@@ -213,9 +214,15 @@ def get_user_info_with_bearer(bearer_token: str, user_management_url, timeout: i
213
214
  headers={"Authorization": f"Bearer {bearer_token}"},
214
215
  timeout=timeout,
215
216
  )
217
+ if response.status_code in (401, 403):
218
+ # Invalid/expired token is an AUTH failure, not an outage — callers
219
+ # (e.g. the HTTP API) must be able to return 401/403, not 503.
220
+ raise PermissionError(
221
+ f"Authentication failed (HTTP {response.status_code}): {response.text}"
222
+ )
216
223
  if response.status_code != 200:
217
224
  raise ConnectionError(
218
- f"Failed to authenticate (HTTP {response.status_code}): {response.text}"
225
+ f"User management error (HTTP {response.status_code}): {response.text}"
219
226
  )
220
227
  return response.json()
221
228
  except requests.RequestException as e:
@@ -353,14 +360,16 @@ class MLOpsManager:
353
360
  def __init__(
354
361
  self,
355
362
  user_token,
356
- CLEARML_API_HOST=None,
357
- CLEARML_WEB_HOST=None,
363
+ CLEARML_API_HOST=None, # deprecated alias of MLOPS_CORE_API_HOST (legacy compat)
364
+ CLEARML_WEB_HOST=None, # deprecated alias of MLOPS_CORE_WEB_HOST (legacy compat)
358
365
  CEPH_ENDPOINT_URL=None,
359
366
  USER_MANAGEMENT_API=None,
360
367
  verbose=False,
361
368
  skip_connection_check=False,
362
369
  ACL_API_HOST=None,
363
- ACL_ENABLED=None
370
+ ACL_ENABLED=None,
371
+ MLOPS_CORE_API_HOST=None,
372
+ MLOPS_CORE_WEB_HOST=None
364
373
  ):
365
374
  self.verbose = verbose
366
375
  if self.verbose:
@@ -373,13 +382,27 @@ class MLOpsManager:
373
382
  self.ALLOWED_CATEGORIES = self.resolve_allowed_categories()
374
383
 
375
384
  self.USER_TOKEN = user_token
376
- self.CLEARML_API_HOST = CLEARML_API_HOST or os.environ.get("CLEARML_API_HOST")
385
+ # Preferred configuration: MLOPS_CORE_* kwargs / env vars. The legacy
386
+ # CLEARML_* kwargs and env vars are still accepted as deprecated
387
+ # fallbacks for backward compatibility; when both are given, the new
388
+ # MLOPS_CORE_* name wins.
389
+ self.MLOPS_CORE_API_HOST = (
390
+ MLOPS_CORE_API_HOST
391
+ or CLEARML_API_HOST # deprecated kwarg (legacy compat)
392
+ or os.environ.get("MLOPS_CORE_API_HOST")
393
+ or os.environ.get("CLEARML_API_HOST") # legacy env var fallback
394
+ )
377
395
  # Ensure we prioritize specific env var for WEB HOST
378
- self.CLEARML_WEB_HOST = CLEARML_WEB_HOST or os.environ.get("CLEARML_WEB_HOST")
379
-
380
- if not self.CLEARML_WEB_HOST:
381
- self.CLEARML_WEB_HOST = self.CLEARML_API_HOST
382
- print(f"[SDK_WARN] CLEARML_WEB_HOST not set. Defaulting to API host: {self.CLEARML_API_HOST}")
396
+ self.MLOPS_CORE_WEB_HOST = (
397
+ MLOPS_CORE_WEB_HOST
398
+ or CLEARML_WEB_HOST # deprecated kwarg (legacy compat)
399
+ or os.environ.get("MLOPS_CORE_WEB_HOST")
400
+ or os.environ.get("CLEARML_WEB_HOST") # legacy env var fallback
401
+ )
402
+
403
+ if not self.MLOPS_CORE_WEB_HOST:
404
+ self.MLOPS_CORE_WEB_HOST = self.MLOPS_CORE_API_HOST
405
+ print(f"[SDK_WARN] MLOPS_CORE_WEB_HOST not set. Defaulting to API host: {self.MLOPS_CORE_API_HOST}")
383
406
  print("[SDK_WARN] Web links generated might be incorrect if API and Web UI ports differ.")
384
407
 
385
408
  self.USER_MANAGEMENT_API = USER_MANAGEMENT_API or os.environ.get("USER_MANAGEMENT_API")
@@ -390,10 +413,10 @@ class MLOpsManager:
390
413
  self.PUBLIC_ACCESS_KEY = os.environ.get("CEPH_ADMIN_ACCESS_KEY")
391
414
  self.PUBLIC_SECRET_KEY = os.environ.get("CEPH_ADMIN_SECRET_KEY")
392
415
 
393
- user_info, self.CLEARML_USERNAME = get_user_metadata(bearer_token=user_token, user_management_url=self.USER_MANAGEMENT_API)
416
+ user_info, self.MLOPS_CORE_USERNAME = get_user_metadata(bearer_token=user_token, user_management_url=self.USER_MANAGEMENT_API)
394
417
 
395
418
  missing_cfg = [k for k, v in {
396
- "CLEARML_API_HOST": self.CLEARML_API_HOST,
419
+ "MLOPS_CORE_API_HOST": self.MLOPS_CORE_API_HOST,
397
420
  "USER_MANAGEMENT_API": self.USER_MANAGEMENT_API,
398
421
  "CEPH_ENDPOINT_URL": self.CEPH_ENDPOINT_URL,
399
422
  }.items() if not v]
@@ -407,18 +430,20 @@ class MLOpsManager:
407
430
  self.CEPH_ADMIN_SECRET_KEY = user_info["s3_secret_key"]
408
431
  self.CEPH_USER_BUCKET = user_info["s3_bucket"]
409
432
 
410
- self.CLEARML_ACCESS_KEY = user_info["clearml_access_key"]
411
- self.CLEARML_SECRET_KEY = user_info["clearml_secret_key"]
433
+ # NOTE: "clearml_access_key" / "clearml_secret_key" are legacy key names
434
+ # from the user-management wire format — do not rename them.
435
+ self.MLOPS_CORE_ACCESS_KEY = user_info["clearml_access_key"]
436
+ self.MLOPS_CORE_SECRET_KEY = user_info["clearml_secret_key"]
412
437
 
413
438
  if not skip_connection_check:
414
439
  if self.verbose:
415
440
  print("[SDK_INFO] Performing health checks...")
416
441
  if not self.check_user_management_service():
417
442
  raise ValueError("User Management API unreachable")
418
- if not self.check_clearml_service():
419
- raise ValueError("ClearML Server down")
420
- if not self.check_clearml_auth():
421
- raise ValueError("ClearML Auth incorrect")
443
+ if not self.check_mlops_core_service():
444
+ raise ValueError("Mlops-Core Server down")
445
+ if not self.check_mlops_core_auth():
446
+ raise ValueError("Mlops-Core Auth incorrect")
422
447
  if self.verbose:
423
448
  print("[SDK_SUCCESS] Health checks passed")
424
449
 
@@ -470,29 +495,29 @@ class MLOpsManager:
470
495
  print("[SDK_INFO] ACL enforcement disabled (bypass mode)")
471
496
 
472
497
  if self.verbose:
473
- print("[SDK_INFO] Authenticating with ClearML...")
498
+ print("[SDK_INFO] Authenticating with Mlops-Core...")
474
499
 
475
- creds = f"{self.CLEARML_ACCESS_KEY}:{self.CLEARML_SECRET_KEY}"
500
+ creds = f"{self.MLOPS_CORE_ACCESS_KEY}:{self.MLOPS_CORE_SECRET_KEY}"
476
501
  auth_header = b64encode(creds.encode("utf-8")).decode("utf-8")
477
502
  res = requests.post(
478
- f"{self.CLEARML_API_HOST}/auth.login",
503
+ f"{self.MLOPS_CORE_API_HOST}/auth.login",
479
504
  headers={"Authorization": f"Basic {auth_header}"},
480
505
  timeout=self.REQUEST_TIMEOUT
481
506
  )
482
507
  if res.status_code != 200:
483
- error_msg = "Failed to authenticate with ClearML"
508
+ error_msg = "Failed to authenticate with Mlops-Core"
484
509
  print(f"[SDK_FAIL] {error_msg}")
485
510
  raise ValueError(error_msg)
486
511
  self.token = res.json()["data"]["token"]
487
512
 
488
513
  if self.verbose:
489
- print("[SDK_SUCCESS] Authenticated with ClearML")
514
+ print("[SDK_SUCCESS] Authenticated with Mlops-Core")
490
515
 
491
516
  self.projects = ProjectsAPI(self._post, verbose=self.verbose)
492
517
  self.models = ModelsAPI(self._post, verbose=self.verbose)
493
518
 
494
519
  projects = self.projects.get_all()
495
- self.project_name = f"project_{self.CLEARML_USERNAME}"
520
+ self.project_name = f"project_{self.MLOPS_CORE_USERNAME}"
496
521
  exists = [p for p in projects if p["name"] == self.project_name]
497
522
  self.project_id = exists[0]["id"] if exists else self.projects.create(self.project_name)["id"]
498
523
 
@@ -505,12 +530,12 @@ class MLOpsManager:
505
530
 
506
531
  headers = {"Authorization": f"Bearer {self.token}"}
507
532
  try:
508
- res = requests.post(f"{self.CLEARML_API_HOST}{path}", headers=headers, json=params, timeout=self.REQUEST_TIMEOUT)
533
+ res = requests.post(f"{self.MLOPS_CORE_API_HOST}{path}", headers=headers, json=params, timeout=self.REQUEST_TIMEOUT)
509
534
  res.raise_for_status()
510
535
 
511
536
  data = res.json()
512
537
  if "data" not in data:
513
- error_msg = f"Unexpected response from ClearML {path}: missing 'data' field"
538
+ error_msg = f"Unexpected response from Mlops-Core {path}: missing 'data' field"
514
539
  print(f"[SDK_ERROR] {error_msg}")
515
540
  raise ValueError(error_msg)
516
541
 
@@ -518,13 +543,18 @@ class MLOpsManager:
518
543
 
519
544
  except requests.exceptions.HTTPError as e:
520
545
  status = e.response.status_code if (hasattr(e, "response") and e.response is not None) else 0
521
- error_msg = f"ClearML returned HTTP {status} for {path}: {e}"
546
+ error_msg = f"Mlops-Core returned HTTP {status} for {path}: {e}"
522
547
  print(f"[SDK_ERROR] {error_msg}")
523
548
  if status in (401, 403):
524
- raise PermissionError(f"ClearML access denied (HTTP {status}) on {path}: {e}") from e
549
+ raise PermissionError(f"Mlops-Core access denied (HTTP {status}) on {path}: {e}") from e
550
+ if 400 <= status < 500:
551
+ # A 4xx is a BAD REQUEST (e.g. nonexistent id) — not an outage.
552
+ # Raising MetadataError (a ValueError) lets callers/API report
553
+ # "not found / invalid" instead of a bogus 503.
554
+ raise MetadataError(error_msg) from e
525
555
  raise ConnectionError(error_msg) from e
526
556
  except requests.exceptions.RequestException as e:
527
- error_msg = f"Network error calling ClearML {path}: {e}"
557
+ error_msg = f"Network error calling Mlops-Core {path}: {e}"
528
558
  print(f"[SDK_ERROR] {error_msg}")
529
559
  raise ConnectionError(error_msg) from e
530
560
  except ValueError:
@@ -546,49 +576,49 @@ class MLOpsManager:
546
576
  print(f"[SDK_FAIL] User Management API Check Failed: {e}")
547
577
  return False
548
578
 
549
- def check_clearml_service(self):
579
+ def check_mlops_core_service(self):
550
580
  try:
551
- r = requests.get(self.CLEARML_API_HOST + "/auth.login", timeout=5)
581
+ r = requests.get(self.MLOPS_CORE_API_HOST + "/auth.login", timeout=5)
552
582
  if r.status_code in [200, 401]:
553
583
  if self.verbose:
554
- print("[SDK_SUCCESS] ClearML Service Reachable")
584
+ print("[SDK_SUCCESS] Mlops-Core Service Reachable")
555
585
  return True
556
586
  raise ValueError(f"Status Code: {r.status_code}")
557
587
  except Exception as e:
558
588
  if self.verbose:
559
- print(f"[SDK_FAIL] ClearML Service Check Failed: {e}")
589
+ print(f"[SDK_FAIL] Mlops-Core Service Check Failed: {e}")
560
590
  return False
561
591
 
562
- def check_clearml_auth(self):
592
+ def check_mlops_core_auth(self):
563
593
  try:
564
- creds = f"{self.CLEARML_ACCESS_KEY}:{self.CLEARML_SECRET_KEY}"
594
+ creds = f"{self.MLOPS_CORE_ACCESS_KEY}:{self.MLOPS_CORE_SECRET_KEY}"
565
595
  auth_header = b64encode(creds.encode("utf-8")).decode("utf-8")
566
596
  r = requests.post(
567
- self.CLEARML_API_HOST + "/auth.login",
597
+ self.MLOPS_CORE_API_HOST + "/auth.login",
568
598
  headers={"Authorization": f"Basic {auth_header}"},
569
599
  timeout=5
570
600
  )
571
601
  if r.status_code == 200:
572
602
  if self.verbose:
573
- print("[SDK_SUCCESS] ClearML Auth Valid")
603
+ print("[SDK_SUCCESS] Mlops-Core Auth Valid")
574
604
  return True
575
605
  raise ValueError(f"Status Code: {r.status_code}")
576
606
  except Exception as e:
577
607
  if self.verbose:
578
- print(f"[SDK_FAIL] ClearML Auth Check Failed: {e}")
608
+ print(f"[SDK_FAIL] Mlops-Core Auth Check Failed: {e}")
579
609
  return False
580
610
 
581
611
  def get_model_id_by_name(self, name, owner=None):
582
612
  """
583
- Resolves a model name to its ClearML ID.
613
+ Resolves a model name to its Mlops-Core ID.
584
614
 
585
615
  owner: if omitted, searches your own project only (original behavior).
586
- If given (a CLEARML username), searches that user's project instead -
616
+ If given (a Mlops-Core username), searches that user's project instead -
587
617
  use this to resolve a model you don't own, e.g. a public model
588
618
  someone else created with the same kind of name.
589
619
  """
590
620
  project_id = self.project_id
591
- if owner and owner != self.CLEARML_USERNAME:
621
+ if owner and owner != self.MLOPS_CORE_USERNAME:
592
622
  target_project_name = f"project_{owner}"
593
623
  match = next((p for p in self.projects.get_all() if p["name"] == target_project_name), None)
594
624
  if not match:
@@ -616,7 +646,7 @@ class MLOpsManager:
616
646
 
617
647
  def _resolve_owner_for_project(self, project_id):
618
648
  if project_id == self.project_id:
619
- return self.CLEARML_USERNAME
649
+ return self.MLOPS_CORE_USERNAME
620
650
  return self._get_project_owner_map().get(project_id, "unknown")
621
651
 
622
652
  def get_model_link(self, model_id):
@@ -631,7 +661,7 @@ class MLOpsManager:
631
661
  except Exception:
632
662
  pass
633
663
 
634
- host = self.CLEARML_WEB_HOST.rstrip('/')
664
+ host = self.MLOPS_CORE_WEB_HOST.rstrip('/')
635
665
  link = f"{host}/projects/{project_id}/models/{model_id}"
636
666
 
637
667
  if self.verbose:
@@ -897,6 +927,7 @@ class MLOpsManager:
897
927
  temp_local_path = None
898
928
  have_model_py = False
899
929
  size_mb = 0.0
930
+ moved_to_final = False
900
931
 
901
932
  try:
902
933
  if self.verbose:
@@ -921,11 +952,41 @@ class MLOpsManager:
921
952
  current_manager.upload(code_path, dest_temp_path + "model.py")
922
953
  have_model_py = True
923
954
 
955
+ # Recovery metadata: a small _meta.json travels WITH the files so a
956
+ # registry entry can be rebuilt from storage alone (see
957
+ # recover_model_from_storage) if the Mlops-Core metadata is ever lost.
958
+ recovery_meta = {
959
+ "model_id": model_id,
960
+ "model_name": model_name,
961
+ "version": version,
962
+ "version_number": next_version_number,
963
+ "category": category,
964
+ "tags": final_tags,
965
+ "owner": self.MLOPS_CORE_USERNAME,
966
+ "public": public,
967
+ "folderName": model_folder_name,
968
+ "haveModelPy": have_model_py,
969
+ "size": size_mb,
970
+ "created": time.strftime("%Y-%m-%d %H:%M:%S"),
971
+ }
972
+ try:
973
+ current_manager.s3.put_object(
974
+ Bucket=current_manager.bucket_name,
975
+ Key=dest_temp_path + "_meta.json",
976
+ Body=json.dumps(recovery_meta).encode("utf-8"),
977
+ ContentType="application/json",
978
+ )
979
+ except Exception as e:
980
+ # best-effort: never fail an add because of the recovery sidecar
981
+ if self.verbose:
982
+ print(f"[SDK_WARN] Could not write recovery _meta.json: {e}")
983
+
924
984
  if current_manager.check_if_exists(dest_version_path):
925
985
  print(f"[SDK_WARN] Version {version} exists. Overwriting...")
926
986
  current_manager.delete_folder(dest_version_path)
927
-
987
+
928
988
  current_manager.move_folder(dest_temp_path, dest_version_path)
989
+ moved_to_final = True # from here on, rollback must clean dest_version_path too
929
990
 
930
991
  model_data = self.models.get_by_id(model_id)
931
992
  metadata = model_data.get("metadata", {})
@@ -985,11 +1046,29 @@ class MLOpsManager:
985
1046
  print("[SDK_INFO] Rolled back new model creation")
986
1047
  except Exception:
987
1048
  pass
1049
+ # ACL was registered before upload — roll it back too so the ACL
1050
+ # service doesn't keep a record of a model that no longer exists.
1051
+ if is_new_model and self.acl:
1052
+ try:
1053
+ self.acl.delete_model(model_id)
1054
+ except Exception:
1055
+ pass
988
1056
  if dest_temp_path and "current_manager" in locals():
989
1057
  try:
990
1058
  current_manager.delete_folder(dest_temp_path)
991
1059
  except Exception:
992
1060
  pass
1061
+ # If files were already moved to the FINAL path but the metadata
1062
+ # write failed, those files would be invisible zombies (metadata
1063
+ # doesn't reference them and the *_tmp_* sweep won't find them).
1064
+ # Delete them so registry and S3 stay consistent.
1065
+ if moved_to_final and "current_manager" in locals():
1066
+ try:
1067
+ current_manager.delete_folder(dest_version_path)
1068
+ if self.verbose:
1069
+ print("[SDK_INFO] Rolled back uploaded files from final path")
1070
+ except Exception:
1071
+ pass
993
1072
  raise
994
1073
  finally:
995
1074
  for path in [local_path, temp_local_path]:
@@ -1207,7 +1286,7 @@ class MLOpsManager:
1207
1286
 
1208
1287
  # 2. If not found by name, assume it might be an ID
1209
1288
  if not model_id:
1210
- # Simple regex check for ClearML ID format (32 hex chars) just to be safe,
1289
+ # Simple regex check for Mlops-Core ID format (32 hex chars) just to be safe,
1211
1290
  # or directly attempt to use it as ID.
1212
1291
  if re.match(r'^[a-f0-9]{32}$', model_identifier):
1213
1292
  model_id = model_identifier
@@ -1216,8 +1295,10 @@ class MLOpsManager:
1216
1295
 
1217
1296
  try:
1218
1297
  model_data = self.models.get_by_id(model_id)
1298
+ except (PermissionError, ConnectionError):
1299
+ raise # outage/auth must NOT be reported as "not found"
1219
1300
  except Exception:
1220
- raise ValueError(f"Model data retrieval failed for ID: {model_id}")
1301
+ raise ModelNotFoundError(f"Model data retrieval failed for ID: {model_id}")
1221
1302
 
1222
1303
  metadata = model_data.get("metadata", {})
1223
1304
  latest_version = self._extract_metadata_value(metadata, "latest_version")
@@ -1253,8 +1334,10 @@ class MLOpsManager:
1253
1334
 
1254
1335
  try:
1255
1336
  data = self.models.get_by_id(model_id)
1337
+ except (PermissionError, ConnectionError):
1338
+ raise # outage/auth must NOT be reported as "not found"
1256
1339
  except Exception:
1257
- raise ValueError(f"No model found with identifier: '{identifier}'")
1340
+ raise ModelNotFoundError(f"No model found with identifier: '{identifier}'")
1258
1341
 
1259
1342
  m = data
1260
1343
  metadata = m.get("metadata", {})
@@ -1440,7 +1523,7 @@ class MLOpsManager:
1440
1523
  print(f"{name:<25} | {mid:<25} | {latest:<10} | {public_display:<6} | {str(total_size):<10} | {display_versions}")
1441
1524
 
1442
1525
  owner_username = (
1443
- self.CLEARML_USERNAME if mid in own_ids
1526
+ self.MLOPS_CORE_USERNAME if mid in own_ids
1444
1527
  else project_owner_map.get(m.get("project"), "unknown")
1445
1528
  )
1446
1529
  model_entry = {
@@ -1504,15 +1587,19 @@ class MLOpsManager:
1504
1587
  path_to_delete = versions_map[version].get("path")
1505
1588
  if not path_to_delete:
1506
1589
  raise ValueError(f"Version '{version}' of model '{model_name or model_id}' has no stored path — metadata may be corrupted")
1507
- ceph_manager.delete_folder(path_to_delete)
1590
+ # Metadata-FIRST ordering: update the registry before deleting S3
1591
+ # files. If we crash in between, the worst case is an orphan S3
1592
+ # folder (harmless, discoverable by verify tooling) — never a
1593
+ # registry entry pointing at deleted files (a zombie the dashboard
1594
+ # would render as a broken model).
1508
1595
  del versions_map[version]
1509
-
1596
+
1510
1597
  if not versions_map:
1511
1598
  print(f"[SDK_INFO] No versions left for model. Deleting entire model...")
1512
- ceph_manager.delete_folder(f"_models/{model_id}/")
1513
1599
  self.models.delete(model_id)
1514
1600
  if self.acl:
1515
1601
  self.acl.delete_model(model_id)
1602
+ ceph_manager.delete_folder(f"_models/{model_id}/")
1516
1603
  print(f"[SDK_SUCCESS] Model '{model_name or model_id}' completely deleted.")
1517
1604
  return
1518
1605
  else:
@@ -1546,12 +1633,216 @@ class MLOpsManager:
1546
1633
  {"key": "haveModelPy", "type": "str", "value": versions_map[new_latest].get("haveModelPy", "false") if new_latest in versions_map else "false"}
1547
1634
  ]
1548
1635
  self.models.update_metadata(model_id, metadata=new_metadata)
1549
-
1636
+
1637
+ # S3 delete LAST (see metadata-first note above).
1638
+ ceph_manager.delete_folder(path_to_delete)
1639
+
1550
1640
  print(f"[SDK_SUCCESS] Version {version} deleted.")
1551
1641
 
1552
1642
  else:
1553
- ceph_manager.delete_folder(f"_models/{model_id}/")
1643
+ # Metadata-first here too: registry entry goes before the files.
1554
1644
  self.models.delete(model_id)
1555
1645
  if self.acl:
1556
1646
  self.acl.delete_model(model_id)
1557
- print(f"[SDK_SUCCESS] Model '{model_id}' deleted completely.")
1647
+ ceph_manager.delete_folder(f"_models/{model_id}/")
1648
+ print(f"[SDK_SUCCESS] Model '{model_id}' deleted completely.")
1649
+
1650
+ # ------------------------------------------------------------------
1651
+ # Registry <-> Storage consistency (zombie/orphan detection & recovery)
1652
+ # ------------------------------------------------------------------
1653
+
1654
+ def verify_model_integrity(self, model_name=None, model_id=None):
1655
+ """
1656
+ Cross-check one model between the registry (Mlops-Core) and S3.
1657
+
1658
+ Returns:
1659
+ {
1660
+ "model_id", "model_name", "status": "ok"|"missing_files"|"orphan_files",
1661
+ "versions": [{"version","path","files_exist","status"}...],
1662
+ "orphan_folders": [<S3 folders under the model not referenced by any version>],
1663
+ }
1664
+ Raises ModelNotFoundError if the model is not in the registry at all.
1665
+ """
1666
+ if not model_id:
1667
+ model_id = self.get_model_id_by_name(model_name)
1668
+ if not model_id:
1669
+ raise ModelNotFoundError(f"Model not found: '{model_name}'")
1670
+ data = self.models.get_by_id(model_id)
1671
+ metadata = data.get("metadata", {})
1672
+ versions_map = self._parse_versions_map(metadata)
1673
+ is_public = self._extract_metadata_value(metadata, "public", "false").lower() == "true"
1674
+ mgr = self._get_ceph_manager(is_public)
1675
+
1676
+ report = {
1677
+ "model_id": model_id,
1678
+ "model_name": data.get("name"),
1679
+ "status": "ok",
1680
+ "versions": [],
1681
+ "orphan_folders": [],
1682
+ }
1683
+ referenced_dirs = set()
1684
+ for ver, info in versions_map.items():
1685
+ if not isinstance(info, dict):
1686
+ continue
1687
+ path = info.get("path") or ""
1688
+ if path:
1689
+ referenced_dirs.add(path.rstrip("/").split("/")[-1])
1690
+ files_exist = bool(path) and mgr.check_if_exists(path)
1691
+ report["versions"].append({
1692
+ "version": ver,
1693
+ "path": path,
1694
+ "files_exist": files_exist,
1695
+ "status": "ok" if files_exist else "missing_files",
1696
+ })
1697
+
1698
+ # S3 folders under this model that no registry version references
1699
+ base = f"_models/{model_id}/"
1700
+ tops = set()
1701
+ try:
1702
+ keys = mgr.list_folder_contents(base) if mgr.check_if_exists(base) else []
1703
+ for k in keys:
1704
+ rel = k[len(base):] if k.startswith(base) else k
1705
+ top = rel.split("/", 1)[0]
1706
+ if top and top != "_meta.json":
1707
+ tops.add(top)
1708
+ except Exception:
1709
+ pass # storage listing unavailable -> report only version checks
1710
+ report["orphan_folders"] = sorted(t for t in tops if t not in referenced_dirs)
1711
+
1712
+ if any(v["status"] != "ok" for v in report["versions"]):
1713
+ report["status"] = "missing_files"
1714
+ elif report["orphan_folders"]:
1715
+ report["status"] = "orphan_files"
1716
+ return report
1717
+
1718
+ def verify_all_models(self):
1719
+ """Run verify_model_integrity over every model in the user's project.
1720
+ Returns a list of per-model reports (never raises per-model; a model
1721
+ that fails to verify is reported with status "error")."""
1722
+ reports = []
1723
+ models = self.models.get_all(self.project_id) or []
1724
+ for m in models:
1725
+ mid = m.get("id")
1726
+ try:
1727
+ reports.append(self.verify_model_integrity(model_id=mid))
1728
+ except (PermissionError, ConnectionError):
1729
+ raise
1730
+ except Exception as e:
1731
+ reports.append({
1732
+ "model_id": mid, "model_name": m.get("name"),
1733
+ "status": "error", "error": str(e),
1734
+ "versions": [], "orphan_folders": [],
1735
+ })
1736
+ return reports
1737
+
1738
+ def find_orphan_models(self):
1739
+ """
1740
+ Find S3 model folders (under `_models/`) that the registry does NOT
1741
+ know about — e.g. leftovers after a registry (Mlops-Core) data loss.
1742
+ Returns [{"model_id", "recoverable" (has _meta.json), "meta": {...}|None}].
1743
+ """
1744
+ mgr = self._get_ceph_manager(False)
1745
+ registry_ids = {m.get("id") for m in (self.models.get_all(self.project_id) or [])}
1746
+ s3_ids = set()
1747
+ try:
1748
+ for k in mgr.list_folder_contents("_models/"):
1749
+ rel = k[len("_models/"):] if k.startswith("_models/") else k
1750
+ top = rel.split("/", 1)[0]
1751
+ if top:
1752
+ s3_ids.add(top)
1753
+ except Exception:
1754
+ pass
1755
+ orphans = []
1756
+ for mid in sorted(s3_ids - registry_ids):
1757
+ meta = self._read_first_meta_json(mgr, mid)
1758
+ orphans.append({"model_id": mid, "recoverable": meta is not None, "meta": meta})
1759
+ return orphans
1760
+
1761
+ def _read_first_meta_json(self, mgr, model_id):
1762
+ """Read any one version's recovery _meta.json for a model, else None."""
1763
+ try:
1764
+ for k in mgr.list_folder_contents(f"_models/{model_id}/"):
1765
+ if k.endswith("/_meta.json"):
1766
+ try:
1767
+ obj = mgr.s3.get_object(Bucket=mgr.bucket_name, Key=k)
1768
+ return json.loads(obj["Body"].read().decode("utf-8"))
1769
+ except Exception:
1770
+ continue
1771
+ except Exception:
1772
+ pass
1773
+ return None
1774
+
1775
+ def recover_model_from_storage(self, model_id):
1776
+ """
1777
+ Rebuild a registry (Mlops-Core) entry for a model that exists in S3 but
1778
+ not in the registry, using the per-version `_meta.json` sidecar files
1779
+ written by add_model. This is the disaster-recovery path when the
1780
+ registry's data is lost while S3 survives.
1781
+
1782
+ Returns the new registry model id.
1783
+ """
1784
+ mgr = self._get_ceph_manager(False)
1785
+ base = f"_models/{model_id}/"
1786
+ try:
1787
+ keys = mgr.list_folder_contents(base)
1788
+ except Exception as e:
1789
+ raise StorageError(f"Cannot list storage for model {model_id}: {e}")
1790
+ # collect every version folder + its _meta.json
1791
+ metas = {}
1792
+ for k in keys:
1793
+ if k.endswith("/_meta.json"):
1794
+ try:
1795
+ obj = mgr.s3.get_object(Bucket=mgr.bucket_name, Key=k)
1796
+ meta = json.loads(obj["Body"].read().decode("utf-8"))
1797
+ metas[meta.get("version")] = (k, meta)
1798
+ except Exception:
1799
+ continue
1800
+ if not metas:
1801
+ raise StorageError(
1802
+ f"No recovery _meta.json found under {base} — this model predates "
1803
+ f"recovery sidecars and must be re-registered manually."
1804
+ )
1805
+ any_meta = next(iter(metas.values()))[1]
1806
+ name = any_meta.get("model_name") or f"recovered_{model_id[:8]}"
1807
+ if self.get_model_id_by_name(name):
1808
+ raise ValueError(f"A model named '{name}' already exists in the registry — refusing to overwrite.")
1809
+
1810
+ tags = any_meta.get("tags") or []
1811
+ category = any_meta.get("category")
1812
+ created_model = self.models.create(name=name, project_id=self.project_id, uri="s3://placeholder", tags=tags)
1813
+ new_id = created_model["id"]
1814
+
1815
+ versions_map, latest, latest_num = {}, None, -1
1816
+ for ver, (key, meta) in metas.items():
1817
+ old_prefix = key[: -len("_meta.json")] # _models/<old_id>/<ver>/
1818
+ versions_map[ver] = {
1819
+ "path": old_prefix,
1820
+ "size": meta.get("size", 0.0),
1821
+ "created": meta.get("created", ""),
1822
+ "haveModelPy": str(meta.get("haveModelPy", False)).lower(),
1823
+ "folderName": meta.get("folderName", ""),
1824
+ "version_number": meta.get("version_number", 0),
1825
+ }
1826
+ try:
1827
+ vn = int(meta.get("version_number", 0))
1828
+ except Exception:
1829
+ vn = 0
1830
+ if vn >= latest_num:
1831
+ latest_num, latest = vn, ver
1832
+
1833
+ total_size = sum(float(v.get("size") or 0) for v in versions_map.values())
1834
+ new_metadata = [
1835
+ {"key": "versions_map", "type": "str", "value": json.dumps(versions_map)},
1836
+ {"key": "latest_version", "type": "str", "value": latest or ""},
1837
+ {"key": "modelSize", "type": "float", "value": f"{total_size:.2f}"},
1838
+ {"key": "version_count", "type": "int", "value": str(len(versions_map))},
1839
+ {"key": "public", "type": "bool", "value": str(any_meta.get("public", False)).lower()},
1840
+ {"key": "recovered_from", "type": "str", "value": model_id},
1841
+ ]
1842
+ if category:
1843
+ new_metadata.append({"key": "category", "type": "str", "value": category})
1844
+ uri = f"s3://{mgr.bucket_name}/{versions_map[latest]['path']}" if latest else "s3://placeholder"
1845
+ self.models.update_info(new_id, uri=uri, tags=tags)
1846
+ self.models.update_metadata(new_id, metadata=new_metadata)
1847
+ print(f"[SDK_SUCCESS] Recovered model '{name}' ({len(versions_map)} versions) as registry id {new_id} (files remain at {base}).")
1848
+ return new_id
@@ -11,7 +11,7 @@ print("\n[STEP 1] Initialize MLOps Manager")
11
11
  # verbose=True enables detailed logging (Transparency Mode)
12
12
  manager = MLOpsManager(
13
13
  user_token=os.getenv("USER_TOKEN"),
14
- CLEARML_API_HOST=os.getenv("CLEARML_API_HOST"),
14
+ MLOPS_CORE_API_HOST=os.getenv("MLOPS_CORE_API_HOST"),
15
15
  CEPH_ENDPOINT_URL=os.getenv("CEPH_ENDPOINT_URL"),
16
16
  USER_MANAGEMENT_API=os.getenv("USER_MANAGEMENT_API"),
17
17
  verbose=True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipmodel
3
- Version: 0.2.67
3
+ Version: 0.2.69
4
4
  Summary: SDK for model registration, versioning, and storage
5
5
  Home-page: https://github.com/AIP-MLOPS/model-registry
6
6
  Author: AIP MLOPS Team
@@ -5,6 +5,7 @@ setup.py
5
5
  aipmodel/CephS3Manager.py
6
6
  aipmodel/__init__.py
7
7
  aipmodel/acl_manager.py
8
+ aipmodel/exceptions.py
8
9
  aipmodel/model_registry.py
9
10
  aipmodel/template.py
10
11
  aipmodel/update_checker.py
@@ -31,7 +31,7 @@ version, description = read_meta()
31
31
 
32
32
  setup(
33
33
  name="aipmodel",
34
- version="0.2.67",
34
+ version="0.2.69",
35
35
  description=description,
36
36
  author="AIP MLOPS Team",
37
37
  author_email="mohmmadweb@gmail.com",
@@ -1,10 +0,0 @@
1
- from .CephS3Manager import CephS3Manager
2
- from .model_registry import MLOpsManager
3
- from .update_checker import check_latest_version
4
-
5
- check_latest_version("aipmodel")
6
-
7
- __all__ = ["MLOpsManager", "CephS3Manager"]
8
-
9
- __version__ = "0.2.67"
10
- __description__ = "SDK for model registration, versioning, and storage"
File without changes
File without changes
File without changes