coreplexml 0.1.1__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.
- coreplexml/__init__.py +21 -0
- coreplexml/_http.py +157 -0
- coreplexml/client.py +76 -0
- coreplexml/datasets.py +145 -0
- coreplexml/deployments.py +163 -0
- coreplexml/exceptions.py +57 -0
- coreplexml/experiments.py +126 -0
- coreplexml/models.py +108 -0
- coreplexml/privacy.py +198 -0
- coreplexml/projects.py +161 -0
- coreplexml/reports.py +118 -0
- coreplexml/studio.py +115 -0
- coreplexml/synthgen.py +171 -0
- coreplexml-0.1.1.dist-info/METADATA +156 -0
- coreplexml-0.1.1.dist-info/RECORD +18 -0
- coreplexml-0.1.1.dist-info/WHEEL +5 -0
- coreplexml-0.1.1.dist-info/licenses/LICENSE +28 -0
- coreplexml-0.1.1.dist-info/top_level.txt +1 -0
coreplexml/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""CorePlexML Python SDK.
|
|
2
|
+
|
|
3
|
+
A Python client for the CorePlexML AutoML & MLOps platform.
|
|
4
|
+
Provides typed access to projects, datasets, experiments, models,
|
|
5
|
+
deployments, reports, privacy suite, synthetic data generation,
|
|
6
|
+
and what-if analysis.
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
from coreplexml import CorePlexMLClient
|
|
11
|
+
|
|
12
|
+
client = CorePlexMLClient("https://your-domain.com", api_key="your-key")
|
|
13
|
+
projects = client.projects.list()
|
|
14
|
+
|
|
15
|
+
Version: 0.1.1
|
|
16
|
+
"""
|
|
17
|
+
from coreplexml.client import CorePlexMLClient
|
|
18
|
+
from coreplexml.exceptions import CorePlexMLError, AuthenticationError, NotFoundError, ValidationError
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.1"
|
|
21
|
+
__all__ = ["CorePlexMLClient", "CorePlexMLError", "AuthenticationError", "NotFoundError", "ValidationError", "__version__"]
|
coreplexml/_http.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HTTP transport layer for the CorePlexML Python SDK.
|
|
4
|
+
|
|
5
|
+
Provides low-level HTTP methods with automatic error handling,
|
|
6
|
+
authentication, retry logic, and job polling support.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from requests.adapters import HTTPAdapter
|
|
15
|
+
from urllib3.util.retry import Retry
|
|
16
|
+
|
|
17
|
+
from coreplexml.exceptions import CorePlexMLError, AuthenticationError, NotFoundError, ValidationError
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_SDK_VERSION = "0.1.1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HTTPClient:
|
|
25
|
+
"""Low-level HTTP client with error handling, retries, and job polling."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str, api_key: str | None = None, timeout: int = 30):
|
|
28
|
+
self.base_url = base_url.rstrip("/")
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
self.session = requests.Session()
|
|
31
|
+
self.session.headers["User-Agent"] = f"coreplexml-python/{_SDK_VERSION}"
|
|
32
|
+
self.session.headers["Content-Type"] = "application/json"
|
|
33
|
+
if api_key:
|
|
34
|
+
self.session.headers["Authorization"] = f"Bearer {api_key}"
|
|
35
|
+
|
|
36
|
+
# Retry transient failures with exponential backoff
|
|
37
|
+
retry = Retry(
|
|
38
|
+
total=3,
|
|
39
|
+
backoff_factor=0.5,
|
|
40
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
41
|
+
allowed_methods=["GET", "PUT", "DELETE", "HEAD", "OPTIONS"],
|
|
42
|
+
)
|
|
43
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
44
|
+
self.session.mount("http://", adapter)
|
|
45
|
+
self.session.mount("https://", adapter)
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
"""Close the underlying HTTP session and release connections."""
|
|
49
|
+
self.session.close()
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return f"HTTPClient(base_url={self.base_url!r})"
|
|
53
|
+
|
|
54
|
+
def _url(self, path: str) -> str:
|
|
55
|
+
return f"{self.base_url}{path}"
|
|
56
|
+
|
|
57
|
+
def _request(self, method: str, path: str, **kwargs) -> requests.Response:
|
|
58
|
+
"""Execute HTTP request with connection error wrapping."""
|
|
59
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
60
|
+
try:
|
|
61
|
+
return self.session.request(method, self._url(path), **kwargs)
|
|
62
|
+
except requests.exceptions.Timeout as e:
|
|
63
|
+
raise CorePlexMLError(f"Request timed out: {e}") from e
|
|
64
|
+
except requests.exceptions.ConnectionError as e:
|
|
65
|
+
raise CorePlexMLError(f"Connection failed: {e}") from e
|
|
66
|
+
except requests.exceptions.RequestException as e:
|
|
67
|
+
raise CorePlexMLError(f"Request failed: {e}") from e
|
|
68
|
+
|
|
69
|
+
def _handle_response(self, resp: requests.Response) -> dict:
|
|
70
|
+
"""Process HTTP response and raise typed exceptions on errors."""
|
|
71
|
+
if resp.status_code == 204:
|
|
72
|
+
return {}
|
|
73
|
+
try:
|
|
74
|
+
data = resp.json()
|
|
75
|
+
except Exception:
|
|
76
|
+
text = resp.text[:500] if resp.text else "No response body"
|
|
77
|
+
data = {"detail": text}
|
|
78
|
+
|
|
79
|
+
if resp.status_code >= 400:
|
|
80
|
+
detail = data.get("detail", str(data)) if isinstance(data, dict) else str(data)
|
|
81
|
+
msg = f"HTTP {resp.status_code}: {detail}"
|
|
82
|
+
if resp.status_code in (401, 403):
|
|
83
|
+
raise AuthenticationError(msg, resp.status_code, detail)
|
|
84
|
+
elif resp.status_code == 404:
|
|
85
|
+
raise NotFoundError(msg, resp.status_code, detail)
|
|
86
|
+
elif resp.status_code == 422:
|
|
87
|
+
raise ValidationError(msg, resp.status_code, detail)
|
|
88
|
+
else:
|
|
89
|
+
raise CorePlexMLError(msg, resp.status_code, detail)
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
def get(self, path: str, params: dict | None = None) -> dict:
|
|
93
|
+
"""Send GET request."""
|
|
94
|
+
resp = self._request("GET", path, params=params)
|
|
95
|
+
return self._handle_response(resp)
|
|
96
|
+
|
|
97
|
+
def post(self, path: str, json: dict | None = None, files: dict | None = None) -> dict:
|
|
98
|
+
"""Send POST request."""
|
|
99
|
+
if files:
|
|
100
|
+
saved_ct = self.session.headers.pop("Content-Type", None)
|
|
101
|
+
try:
|
|
102
|
+
resp = self._request("POST", path, data=json, files=files)
|
|
103
|
+
finally:
|
|
104
|
+
if saved_ct:
|
|
105
|
+
self.session.headers["Content-Type"] = saved_ct
|
|
106
|
+
else:
|
|
107
|
+
resp = self._request("POST", path, json=json)
|
|
108
|
+
return self._handle_response(resp)
|
|
109
|
+
|
|
110
|
+
def put(self, path: str, json: dict | None = None) -> dict:
|
|
111
|
+
"""Send PUT request."""
|
|
112
|
+
resp = self._request("PUT", path, json=json)
|
|
113
|
+
return self._handle_response(resp)
|
|
114
|
+
|
|
115
|
+
def patch(self, path: str, json: dict | None = None) -> dict:
|
|
116
|
+
"""Send PATCH request."""
|
|
117
|
+
resp = self._request("PATCH", path, json=json)
|
|
118
|
+
return self._handle_response(resp)
|
|
119
|
+
|
|
120
|
+
def delete(self, path: str) -> dict:
|
|
121
|
+
"""Send DELETE request."""
|
|
122
|
+
resp = self._request("DELETE", path)
|
|
123
|
+
return self._handle_response(resp)
|
|
124
|
+
|
|
125
|
+
def download(self, path: str, output_path: str, params: dict | None = None) -> str:
|
|
126
|
+
"""Download a file from the API.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The output_path where the file was saved.
|
|
130
|
+
"""
|
|
131
|
+
resp = self._request("GET", path, params=params, stream=True)
|
|
132
|
+
if resp.status_code >= 400:
|
|
133
|
+
self._handle_response(resp) # raises typed exception
|
|
134
|
+
with open(output_path, "wb") as f:
|
|
135
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
136
|
+
f.write(chunk)
|
|
137
|
+
return output_path
|
|
138
|
+
|
|
139
|
+
def poll_job(self, job_id: str, interval: float = 3.0, timeout: float = 600.0) -> dict:
|
|
140
|
+
"""Poll a job until completion or timeout."""
|
|
141
|
+
start = time.time()
|
|
142
|
+
while time.time() - start < timeout:
|
|
143
|
+
data = self.get(f"/api/jobs/{job_id}")
|
|
144
|
+
job = data.get("job", data) if isinstance(data, dict) else {}
|
|
145
|
+
status = job.get("status", "") if isinstance(job, dict) else ""
|
|
146
|
+
if status in ("succeeded", "completed", "failed", "error"):
|
|
147
|
+
return job if isinstance(job, dict) else data
|
|
148
|
+
time.sleep(interval)
|
|
149
|
+
raise CorePlexMLError(f"Job {job_id} timed out after {timeout}s")
|
|
150
|
+
|
|
151
|
+
def upload(self, path: str, file_path: str, fields: dict | None = None) -> dict:
|
|
152
|
+
"""Upload a file via multipart form."""
|
|
153
|
+
import os
|
|
154
|
+
filename = os.path.basename(file_path)
|
|
155
|
+
with open(file_path, "rb") as f:
|
|
156
|
+
files = {"file": (filename, f)}
|
|
157
|
+
return self.post(path, json=fields, files=files)
|
coreplexml/client.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Main CorePlexML client."""
|
|
4
|
+
|
|
5
|
+
from coreplexml._http import HTTPClient
|
|
6
|
+
from coreplexml.projects import ProjectsResource
|
|
7
|
+
from coreplexml.datasets import DatasetsResource
|
|
8
|
+
from coreplexml.experiments import ExperimentsResource
|
|
9
|
+
from coreplexml.models import ModelsResource
|
|
10
|
+
from coreplexml.deployments import DeploymentsResource
|
|
11
|
+
from coreplexml.reports import ReportsResource
|
|
12
|
+
from coreplexml.privacy import PrivacyResource
|
|
13
|
+
from coreplexml.synthgen import SynthGenResource
|
|
14
|
+
from coreplexml.studio import StudioResource
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CorePlexMLClient:
|
|
18
|
+
"""Top-level client for the CorePlexML Python SDK.
|
|
19
|
+
|
|
20
|
+
Provides access to all API resources through typed sub-clients.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
base_url: CorePlexML server URL (default ``http://localhost:8888``).
|
|
24
|
+
api_key: Bearer token for API authentication.
|
|
25
|
+
timeout: Request timeout in seconds (default 30).
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
from coreplexml import CorePlexMLClient
|
|
30
|
+
|
|
31
|
+
client = CorePlexMLClient("https://your-domain.com", api_key="your-key")
|
|
32
|
+
projects = client.projects.list()
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
projects: Manage projects.
|
|
36
|
+
datasets: Upload, list, and manage datasets.
|
|
37
|
+
experiments: Create and monitor AutoML experiments.
|
|
38
|
+
models: Get model info and make predictions.
|
|
39
|
+
deployments: Deploy models and manage production endpoints.
|
|
40
|
+
reports: Generate and download reports.
|
|
41
|
+
privacy: Privacy Suite -- PII detection and data transformation.
|
|
42
|
+
synthgen: Synthetic data generation with CTGAN/CopulaGAN/TVAE/Gaussian Copula.
|
|
43
|
+
studio: What-If Analysis -- scenario comparison.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, base_url: str = "http://localhost:8888", api_key: str | None = None, timeout: int = 30):
|
|
47
|
+
"""Initialize the CorePlexML client.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: Server URL (default ``http://localhost:8888``).
|
|
51
|
+
api_key: API key for Bearer authentication.
|
|
52
|
+
timeout: Request timeout in seconds.
|
|
53
|
+
"""
|
|
54
|
+
self._http = HTTPClient(base_url, api_key=api_key, timeout=timeout)
|
|
55
|
+
self.projects = ProjectsResource(self._http)
|
|
56
|
+
self.datasets = DatasetsResource(self._http)
|
|
57
|
+
self.experiments = ExperimentsResource(self._http)
|
|
58
|
+
self.models = ModelsResource(self._http)
|
|
59
|
+
self.deployments = DeploymentsResource(self._http)
|
|
60
|
+
self.reports = ReportsResource(self._http)
|
|
61
|
+
self.privacy = PrivacyResource(self._http)
|
|
62
|
+
self.synthgen = SynthGenResource(self._http)
|
|
63
|
+
self.studio = StudioResource(self._http)
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
"""Close the underlying HTTP session and release connections."""
|
|
67
|
+
self._http.close()
|
|
68
|
+
|
|
69
|
+
def __enter__(self):
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, *args):
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
return f"CorePlexMLClient(base_url={self._http.base_url!r})"
|
coreplexml/datasets.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Datasets resource for the CorePlexML SDK."""
|
|
4
|
+
from coreplexml._http import HTTPClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DatasetsResource:
|
|
8
|
+
"""Manage datasets and dataset versions.
|
|
9
|
+
|
|
10
|
+
Datasets are the foundation for training experiments. Upload CSV files
|
|
11
|
+
and CorePlexML will version, profile, and analyze them automatically.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, http: HTTPClient):
|
|
15
|
+
self._http = http
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _normalize_dataset_upload(payload: dict) -> dict:
|
|
19
|
+
"""Normalize upload response keys for docs/examples compatibility."""
|
|
20
|
+
if not isinstance(payload, dict):
|
|
21
|
+
return {}
|
|
22
|
+
out = dict(payload)
|
|
23
|
+
if "id" not in out and out.get("dataset_id"):
|
|
24
|
+
out["id"] = out["dataset_id"]
|
|
25
|
+
if "version_id" not in out and out.get("dataset_version_id"):
|
|
26
|
+
out["version_id"] = out["dataset_version_id"]
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
def list(self, project_id: str | None = None, limit: int = 50, offset: int = 0) -> dict:
|
|
30
|
+
"""List datasets, optionally filtered by project.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
project_id: Filter by project UUID (optional).
|
|
34
|
+
limit: Maximum results (default 50).
|
|
35
|
+
offset: Pagination offset.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary with ``items`` list and ``total`` count.
|
|
39
|
+
"""
|
|
40
|
+
params: dict = {"limit": limit, "offset": offset}
|
|
41
|
+
if project_id:
|
|
42
|
+
params["project_id"] = project_id
|
|
43
|
+
return self._http.get("/api/datasets", params=params)
|
|
44
|
+
|
|
45
|
+
def upload(self, project_id: str, file_path: str, name: str, description: str = "") -> dict:
|
|
46
|
+
"""Upload a CSV file as a new dataset.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
project_id: UUID of the owning project.
|
|
50
|
+
file_path: Local path to the CSV file.
|
|
51
|
+
name: Display name for the dataset.
|
|
52
|
+
description: Optional description.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Created dataset dictionary with ``id``, ``name``, etc.
|
|
56
|
+
"""
|
|
57
|
+
data = self._http.upload(
|
|
58
|
+
"/api/datasets/upload",
|
|
59
|
+
file_path,
|
|
60
|
+
fields={"project_id": project_id, "name": name, "description": description},
|
|
61
|
+
)
|
|
62
|
+
return self._normalize_dataset_upload(data)
|
|
63
|
+
|
|
64
|
+
def get(self, dataset_id: str) -> dict:
|
|
65
|
+
"""Get dataset details by ID.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
dataset_id: UUID of the dataset.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dataset dictionary.
|
|
72
|
+
"""
|
|
73
|
+
return self._http.get(f"/api/datasets/{dataset_id}")
|
|
74
|
+
|
|
75
|
+
def versions(self, dataset_id: str) -> dict:
|
|
76
|
+
"""List all versions of a dataset.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
dataset_id: UUID of the dataset.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary with paginated ``items`` list plus ``total``, ``limit``, and ``offset``.
|
|
83
|
+
"""
|
|
84
|
+
return self._http.get(f"/api/datasets/{dataset_id}/versions")
|
|
85
|
+
|
|
86
|
+
def quality(self, dataset_id: str) -> dict:
|
|
87
|
+
"""Get data quality report for a dataset.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
dataset_id: UUID of the dataset.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Quality metrics dictionary.
|
|
94
|
+
"""
|
|
95
|
+
return self._http.get(f"/api/datasets/{dataset_id}/quality")
|
|
96
|
+
|
|
97
|
+
def columns(self, dataset_id: str) -> dict:
|
|
98
|
+
"""Get column metadata for a dataset.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
dataset_id: UUID of the dataset.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dictionary with ``columns`` list.
|
|
105
|
+
"""
|
|
106
|
+
return self._http.get(f"/api/datasets/{dataset_id}/columns")
|
|
107
|
+
|
|
108
|
+
def analyze(self, dataset_id: str) -> dict:
|
|
109
|
+
"""Run statistical analysis on a dataset.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
dataset_id: UUID of the dataset.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Analysis results dictionary.
|
|
116
|
+
"""
|
|
117
|
+
return self._http.get(f"/api/datasets/{dataset_id}/analyze")
|
|
118
|
+
|
|
119
|
+
def delete(self, dataset_id: str) -> dict:
|
|
120
|
+
"""Delete a dataset.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
dataset_id: UUID of the dataset.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Empty dictionary on success.
|
|
127
|
+
"""
|
|
128
|
+
return self._http.delete(f"/api/datasets/{dataset_id}")
|
|
129
|
+
|
|
130
|
+
def download(self, dataset_id: str, output_path: str, format: str = "csv") -> str:
|
|
131
|
+
"""Download dataset to a local file.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
dataset_id: UUID of the dataset.
|
|
135
|
+
output_path: Local path to save the file.
|
|
136
|
+
format: Output format -- ``csv`` or ``parquet`` (default ``csv``).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The output_path on success.
|
|
140
|
+
"""
|
|
141
|
+
return self._http.download(
|
|
142
|
+
f"/api/datasets/{dataset_id}/download",
|
|
143
|
+
output_path,
|
|
144
|
+
params={"format": format},
|
|
145
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Deployments resource for the CorePlexML SDK."""
|
|
4
|
+
from typing import Union
|
|
5
|
+
from coreplexml._http import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeploymentsResource:
|
|
9
|
+
"""Deploy models to production endpoints.
|
|
10
|
+
|
|
11
|
+
Deployments create REST API endpoints for real-time predictions,
|
|
12
|
+
with support for staging/production stages and canary rollouts.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, http: HTTPClient):
|
|
16
|
+
self._http = http
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _normalize_deployment(payload: dict) -> dict:
|
|
20
|
+
"""Normalize API payloads to a direct deployment object."""
|
|
21
|
+
if not isinstance(payload, dict):
|
|
22
|
+
return {}
|
|
23
|
+
dep = payload.get("deployment")
|
|
24
|
+
if isinstance(dep, dict):
|
|
25
|
+
out = dict(dep)
|
|
26
|
+
# Preserve model payload when present for callers that need it.
|
|
27
|
+
if isinstance(payload.get("model"), dict):
|
|
28
|
+
out["_model"] = payload["model"]
|
|
29
|
+
return out
|
|
30
|
+
return payload
|
|
31
|
+
|
|
32
|
+
def list(self, project_id: str, limit: int = 50, offset: int = 0) -> dict:
|
|
33
|
+
"""List deployments for a project.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
project_id: UUID of the project.
|
|
37
|
+
limit: Maximum results (default 50).
|
|
38
|
+
offset: Pagination offset.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dictionary with ``items`` list and ``total`` count.
|
|
42
|
+
"""
|
|
43
|
+
return self._http.get(f"/api/mlops/projects/{project_id}/deployments", params={"limit": limit, "offset": offset})
|
|
44
|
+
|
|
45
|
+
def create(self, project_id: str, model_id: str, name: str, stage: str = "staging", config: dict | None = None) -> dict:
|
|
46
|
+
"""Create a new deployment.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
project_id: UUID of the project.
|
|
50
|
+
model_id: UUID of the model to deploy.
|
|
51
|
+
name: Deployment name.
|
|
52
|
+
stage: Deployment stage -- ``staging`` or ``production`` (default ``staging``).
|
|
53
|
+
config: Optional deployment configuration.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Created deployment dictionary.
|
|
57
|
+
"""
|
|
58
|
+
body: dict = {"model_id": model_id, "name": name, "stage": stage}
|
|
59
|
+
if config:
|
|
60
|
+
body["config"] = config
|
|
61
|
+
data = self._http.post(f"/api/mlops/projects/{project_id}/deployments", json=body)
|
|
62
|
+
return self._normalize_deployment(data)
|
|
63
|
+
|
|
64
|
+
def get(self, deployment_id: str) -> dict:
|
|
65
|
+
"""Get deployment details.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
deployment_id: UUID of the deployment.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Deployment dictionary.
|
|
72
|
+
"""
|
|
73
|
+
data = self._http.get(f"/api/mlops/deployments/{deployment_id}")
|
|
74
|
+
return self._normalize_deployment(data)
|
|
75
|
+
|
|
76
|
+
def predict(self, deployment_id: str, inputs: Union[dict, list], options: dict | None = None) -> dict:
|
|
77
|
+
"""Make predictions via a deployed model endpoint.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
deployment_id: UUID of the deployment.
|
|
81
|
+
inputs: Feature values -- a dict or list of dicts.
|
|
82
|
+
options: Optional prediction options.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Prediction results dictionary.
|
|
86
|
+
"""
|
|
87
|
+
body = {"inputs": inputs, "options": options or {}}
|
|
88
|
+
data = self._http.post(f"/api/mlops/deployments/{deployment_id}/predict", json=body)
|
|
89
|
+
# Convenience aliases for single-row predictions used in quickstart docs.
|
|
90
|
+
if isinstance(inputs, dict) and isinstance(data, dict):
|
|
91
|
+
preds = data.get("predictions")
|
|
92
|
+
if isinstance(preds, list) and preds:
|
|
93
|
+
first = preds[0] if isinstance(preds[0], dict) else {}
|
|
94
|
+
if "prediction" in first and "prediction" not in data:
|
|
95
|
+
data["prediction"] = first.get("prediction")
|
|
96
|
+
if "probability" in first and "probability" not in data:
|
|
97
|
+
data["probability"] = first.get("probability")
|
|
98
|
+
if "probabilities" in first and "probabilities" not in data:
|
|
99
|
+
data["probabilities"] = first.get("probabilities")
|
|
100
|
+
return data
|
|
101
|
+
|
|
102
|
+
def promote(self, deployment_id: str) -> dict:
|
|
103
|
+
"""Promote a staging deployment to production.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
deployment_id: UUID of the deployment.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Updated deployment dictionary.
|
|
110
|
+
"""
|
|
111
|
+
# Backend requires explicit target stage.
|
|
112
|
+
data = self._http.post(
|
|
113
|
+
f"/api/mlops/deployments/{deployment_id}/promote",
|
|
114
|
+
json={"to_stage": "production"},
|
|
115
|
+
)
|
|
116
|
+
return self._normalize_deployment(data)
|
|
117
|
+
|
|
118
|
+
def rollback(
|
|
119
|
+
self,
|
|
120
|
+
deployment_id: str,
|
|
121
|
+
to_deployment_id: str | None = None,
|
|
122
|
+
to_model_id: str | None = None,
|
|
123
|
+
) -> dict:
|
|
124
|
+
"""Rollback a deployment to the previous version.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
deployment_id: UUID of the deployment.
|
|
128
|
+
to_deployment_id: Optional target deployment UUID.
|
|
129
|
+
to_model_id: Optional target model UUID.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Updated deployment dictionary.
|
|
133
|
+
"""
|
|
134
|
+
body: dict = {}
|
|
135
|
+
if to_deployment_id:
|
|
136
|
+
body["to_deployment_id"] = to_deployment_id
|
|
137
|
+
if to_model_id:
|
|
138
|
+
body["to_model_id"] = to_model_id
|
|
139
|
+
data = self._http.post(f"/api/mlops/deployments/{deployment_id}/rollback", json=body)
|
|
140
|
+
return self._normalize_deployment(data)
|
|
141
|
+
|
|
142
|
+
def deactivate(self, deployment_id: str) -> dict:
|
|
143
|
+
"""Deactivate a deployment.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
deployment_id: UUID of the deployment.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Updated deployment dictionary.
|
|
150
|
+
"""
|
|
151
|
+
data = self._http.post(f"/api/mlops/deployments/{deployment_id}/deactivate")
|
|
152
|
+
return self._normalize_deployment(data)
|
|
153
|
+
|
|
154
|
+
def drift(self, deployment_id: str) -> dict:
|
|
155
|
+
"""Get drift detection results for a deployment.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
deployment_id: UUID of the deployment.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Drift metrics dictionary.
|
|
162
|
+
"""
|
|
163
|
+
return self._http.get(f"/api/mlops/deployments/{deployment_id}/drift")
|
coreplexml/exceptions.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Exception classes for the CorePlexML Python SDK.
|
|
4
|
+
|
|
5
|
+
All SDK exceptions inherit from :class:`CorePlexMLError`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CorePlexMLError(Exception):
|
|
10
|
+
"""Base exception for all CorePlexML SDK errors.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
message: Human-readable error message.
|
|
14
|
+
status_code: HTTP status code (if applicable).
|
|
15
|
+
detail: Detailed error information from the server.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str, status_code: int | None = None, detail: str | None = None):
|
|
19
|
+
"""Initialize error.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
message: Error description.
|
|
23
|
+
status_code: HTTP status code.
|
|
24
|
+
detail: Server error detail.
|
|
25
|
+
"""
|
|
26
|
+
self.message = message
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.detail = detail
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthenticationError(CorePlexMLError):
|
|
33
|
+
"""Raised when authentication fails (HTTP 401 or 403).
|
|
34
|
+
|
|
35
|
+
This indicates an invalid, expired, or missing API key.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotFoundError(CorePlexMLError):
|
|
42
|
+
"""Raised when a resource is not found (HTTP 404).
|
|
43
|
+
|
|
44
|
+
The requested resource (project, dataset, model, etc.) does not exist
|
|
45
|
+
or is not accessible to the current user.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ValidationError(CorePlexMLError):
|
|
52
|
+
"""Raised when input validation fails (HTTP 422).
|
|
53
|
+
|
|
54
|
+
The request body or parameters did not pass server-side validation.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
pass
|