mlops-python-sdk 1.0.0__py3-none-any.whl → 1.0.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.
- mlops/__init__.py +3 -3
- mlops/api/client/api/storage/__init__.py +1 -0
- mlops/api/client/api/storage/get_storage_presign_download.py +175 -0
- mlops/api/client/api/storage/get_storage_presign_upload.py +175 -0
- mlops/api/client/api/tasks/cancel_task.py +14 -14
- mlops/api/client/api/tasks/delete_task.py +14 -14
- mlops/api/client/api/tasks/get_task.py +15 -15
- mlops/api/client/api/tasks/get_task_by_task_id.py +204 -0
- mlops/api/client/api/tasks/get_task_logs.py +300 -0
- mlops/api/client/api/tasks/list_tasks.py +14 -14
- mlops/api/client/models/__init__.py +16 -0
- mlops/api/client/models/get_storage_presign_download_response_200.py +60 -0
- mlops/api/client/models/get_storage_presign_upload_response_200.py +79 -0
- mlops/api/client/models/get_task_logs_direction.py +9 -0
- mlops/api/client/models/get_task_logs_log_type.py +10 -0
- mlops/api/client/models/log_pagination.py +90 -0
- mlops/api/client/models/task_log_entry.py +105 -0
- mlops/api/client/models/task_log_entry_log_type.py +9 -0
- mlops/api/client/models/task_logs_response.py +112 -0
- mlops/api/client/models/task_submit_request.py +6 -6
- mlops/connection_config.py +0 -7
- mlops/exceptions.py +10 -10
- mlops/task/__init__.py +1 -1
- mlops/task/client.py +11 -35
- mlops/task/task.py +152 -34
- {mlops_python_sdk-1.0.0.dist-info → mlops_python_sdk-1.0.1.dist-info}/METADATA +3 -12
- mlops_python_sdk-1.0.1.dist-info/RECORD +52 -0
- mlops_python_sdk-1.0.0.dist-info/RECORD +0 -39
- {mlops_python_sdk-1.0.0.dist-info → mlops_python_sdk-1.0.1.dist-info}/WHEEL +0 -0
|
@@ -19,7 +19,7 @@ class TaskSubmitRequest:
|
|
|
19
19
|
"""Task submission request
|
|
20
20
|
|
|
21
21
|
Attributes:
|
|
22
|
-
|
|
22
|
+
cluster_name (str): Slurm cluster name to submit task to Example: slurm-prod.
|
|
23
23
|
name (str): Task name Example: training-job.
|
|
24
24
|
account (Union[None, Unset, str]): Account Example: research.
|
|
25
25
|
command (Union[None, Unset, str]): Command to execute (alternative to script) Example: python train.py.
|
|
@@ -55,7 +55,7 @@ class TaskSubmitRequest:
|
|
|
55
55
|
tres (Union[None, Unset, str]): Trackable resources string Example: cpu=4,mem=8G.
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
cluster_name: str
|
|
59
59
|
name: str
|
|
60
60
|
account: Union[None, Unset, str] = UNSET
|
|
61
61
|
command: Union[None, Unset, str] = UNSET
|
|
@@ -91,7 +91,7 @@ class TaskSubmitRequest:
|
|
|
91
91
|
def to_dict(self) -> dict[str, Any]:
|
|
92
92
|
from ..models.task_submit_request_environment_type_0 import TaskSubmitRequestEnvironmentType0
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
cluster_name = self.cluster_name
|
|
95
95
|
|
|
96
96
|
name = self.name
|
|
97
97
|
|
|
@@ -269,7 +269,7 @@ class TaskSubmitRequest:
|
|
|
269
269
|
field_dict.update(self.additional_properties)
|
|
270
270
|
field_dict.update(
|
|
271
271
|
{
|
|
272
|
-
"
|
|
272
|
+
"cluster_name": cluster_name,
|
|
273
273
|
"name": name,
|
|
274
274
|
}
|
|
275
275
|
)
|
|
@@ -340,7 +340,7 @@ class TaskSubmitRequest:
|
|
|
340
340
|
from ..models.task_submit_request_environment_type_0 import TaskSubmitRequestEnvironmentType0
|
|
341
341
|
|
|
342
342
|
d = dict(src_dict)
|
|
343
|
-
|
|
343
|
+
cluster_name = d.pop("cluster_name")
|
|
344
344
|
|
|
345
345
|
name = d.pop("name")
|
|
346
346
|
|
|
@@ -605,7 +605,7 @@ class TaskSubmitRequest:
|
|
|
605
605
|
tres = _parse_tres(d.pop("tres", UNSET))
|
|
606
606
|
|
|
607
607
|
task_submit_request = cls(
|
|
608
|
-
|
|
608
|
+
cluster_name=cluster_name,
|
|
609
609
|
name=name,
|
|
610
610
|
account=account,
|
|
611
611
|
command=command,
|
mlops/connection_config.py
CHANGED
|
@@ -29,10 +29,6 @@ class ConnectionConfig:
|
|
|
29
29
|
def _api_key():
|
|
30
30
|
return os.getenv('MLOPS_API_KEY')
|
|
31
31
|
|
|
32
|
-
@staticmethod
|
|
33
|
-
def _access_token():
|
|
34
|
-
return os.getenv('MLOPS_ACCESS_TOKEN')
|
|
35
|
-
|
|
36
32
|
@staticmethod
|
|
37
33
|
def _api_path():
|
|
38
34
|
return os.getenv('MLOPS_API_PATH', DEFAULT_API_PATH)
|
|
@@ -42,7 +38,6 @@ class ConnectionConfig:
|
|
|
42
38
|
domain: Optional[str] = None,
|
|
43
39
|
debug: Optional[bool] = None,
|
|
44
40
|
api_key: Optional[str] = None,
|
|
45
|
-
access_token: Optional[str] = None,
|
|
46
41
|
request_timeout: Optional[float] = None,
|
|
47
42
|
headers: Optional[Dict[str, str]] = None,
|
|
48
43
|
proxy: Optional[ProxyTypes] = None,
|
|
@@ -51,11 +46,9 @@ class ConnectionConfig:
|
|
|
51
46
|
self.domain = domain or ConnectionConfig._domain()
|
|
52
47
|
self.debug = debug or ConnectionConfig._debug()
|
|
53
48
|
self.api_key = api_key or ConnectionConfig._api_key()
|
|
54
|
-
self.access_token = access_token or ConnectionConfig._access_token()
|
|
55
49
|
self.headers = headers or {}
|
|
56
50
|
self.proxy = proxy
|
|
57
51
|
self.api_path = api_path or ConnectionConfig._api_path()
|
|
58
|
-
|
|
59
52
|
self.request_timeout = ConnectionConfig._get_request_timeout(
|
|
60
53
|
REQUEST_TIMEOUT,
|
|
61
54
|
request_timeout,
|
mlops/exceptions.py
CHANGED
|
@@ -10,17 +10,17 @@ def format_execution_timeout_error() -> Exception:
|
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class MLOpsException(Exception):
|
|
14
14
|
"""
|
|
15
|
-
Base class for all
|
|
15
|
+
Base class for all MLOps errors.
|
|
16
16
|
|
|
17
|
-
Raised when a general
|
|
17
|
+
Raised when a general MLOps exception occurs.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
pass
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class TimeoutException(
|
|
23
|
+
class TimeoutException(MLOpsException):
|
|
24
24
|
"""
|
|
25
25
|
Raised when a timeout occurs.
|
|
26
26
|
|
|
@@ -33,7 +33,7 @@ class TimeoutException(XClientException):
|
|
|
33
33
|
pass
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class InvalidArgumentException(
|
|
36
|
+
class InvalidArgumentException(MLOpsException):
|
|
37
37
|
"""
|
|
38
38
|
Raised when an invalid argument is provided.
|
|
39
39
|
"""
|
|
@@ -41,7 +41,7 @@ class InvalidArgumentException(XClientException):
|
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
class NotEnoughSpaceException(
|
|
44
|
+
class NotEnoughSpaceException(MLOpsException):
|
|
45
45
|
"""
|
|
46
46
|
Raised when there is not enough disk space.
|
|
47
47
|
"""
|
|
@@ -49,7 +49,7 @@ class NotEnoughSpaceException(XClientException):
|
|
|
49
49
|
pass
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
class NotFoundException(
|
|
52
|
+
class NotFoundException(MLOpsException):
|
|
53
53
|
"""
|
|
54
54
|
Raised when a resource is not found.
|
|
55
55
|
"""
|
|
@@ -57,7 +57,7 @@ class NotFoundException(XClientException):
|
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
class AuthenticationException(
|
|
60
|
+
class AuthenticationException(MLOpsException):
|
|
61
61
|
"""
|
|
62
62
|
Raised when authentication fails.
|
|
63
63
|
"""
|
|
@@ -65,7 +65,7 @@ class AuthenticationException(XClientException):
|
|
|
65
65
|
pass
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
class RateLimitException(
|
|
68
|
+
class RateLimitException(MLOpsException):
|
|
69
69
|
"""
|
|
70
70
|
Raised when the API rate limit is exceeded.
|
|
71
71
|
"""
|
|
@@ -73,7 +73,7 @@ class RateLimitException(XClientException):
|
|
|
73
73
|
pass
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
class APIException(
|
|
76
|
+
class APIException(MLOpsException):
|
|
77
77
|
"""
|
|
78
78
|
Raised when an API error occurs.
|
|
79
79
|
"""
|
mlops/task/__init__.py
CHANGED
mlops/task/client.py
CHANGED
|
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def handle_api_exception(e: Response):
|
|
20
|
-
"""Handle API exceptions and convert them to appropriate
|
|
20
|
+
"""Handle API exceptions and convert them to appropriate MLOps exceptions."""
|
|
21
21
|
try:
|
|
22
22
|
body = json.loads(e.content) if e.content else {}
|
|
23
23
|
except json.JSONDecodeError:
|
|
@@ -49,51 +49,27 @@ def handle_api_exception(e: Response):
|
|
|
49
49
|
|
|
50
50
|
class TaskClient(AuthenticatedClient):
|
|
51
51
|
"""
|
|
52
|
-
The client for interacting with the
|
|
52
|
+
The client for interacting with the MLOps Task API.
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
def __init__(
|
|
56
56
|
self,
|
|
57
57
|
config: ConnectionConfig,
|
|
58
|
-
require_api_key: bool = True,
|
|
59
|
-
require_access_token: bool = False,
|
|
60
58
|
limits: Optional[Limits] = None,
|
|
61
59
|
*args,
|
|
62
60
|
**kwargs,
|
|
63
61
|
):
|
|
64
|
-
|
|
62
|
+
# NOTE: This SDK client only supports API key authentication.
|
|
63
|
+
# Header: X-API-Key (per OpenAPI spec)
|
|
64
|
+
if config.api_key is None:
|
|
65
65
|
raise AuthenticationException(
|
|
66
|
-
"
|
|
66
|
+
"API key is required. "
|
|
67
|
+
"You can set the environment variable `MLOPS_API_KEY`"
|
|
68
|
+
' to pass it directly like ConnectionConfig(api_key="mlops_...")',
|
|
67
69
|
)
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"Either api_key or access_token is required",
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
token = None
|
|
75
|
-
if require_api_key:
|
|
76
|
-
if config.api_key is None:
|
|
77
|
-
raise AuthenticationException(
|
|
78
|
-
"API key is required. "
|
|
79
|
-
"You can either set the environment variable `XCLIENT_API_KEY` "
|
|
80
|
-
'or pass it directly like TaskClient(api_key="xclient_...")',
|
|
81
|
-
)
|
|
82
|
-
token = config.api_key
|
|
83
|
-
|
|
84
|
-
if require_access_token:
|
|
85
|
-
if config.access_token is None:
|
|
86
|
-
raise AuthenticationException(
|
|
87
|
-
"Access token is required. "
|
|
88
|
-
"You can set the environment variable `XCLIENT_ACCESS_TOKEN` "
|
|
89
|
-
"or pass the `access_token` in options.",
|
|
90
|
-
)
|
|
91
|
-
token = config.access_token
|
|
92
|
-
|
|
93
|
-
# API Key header: X-API-Key (per OpenAPI spec)
|
|
94
|
-
# JWT header: Authorization: Bearer <token>
|
|
95
|
-
auth_header_name = "X-API-Key" if require_api_key else "Authorization"
|
|
96
|
-
prefix = "" if require_api_key else "Bearer"
|
|
71
|
+
auth_header_name = "X-API-Key"
|
|
72
|
+
prefix = ""
|
|
97
73
|
|
|
98
74
|
headers = {
|
|
99
75
|
**(config.headers or {}),
|
|
@@ -116,9 +92,9 @@ class TaskClient(AuthenticatedClient):
|
|
|
116
92
|
base_url=config.api_url,
|
|
117
93
|
httpx_args=httpx_args,
|
|
118
94
|
headers=headers,
|
|
119
|
-
token=token,
|
|
120
95
|
auth_header_name=auth_header_name,
|
|
121
96
|
prefix=prefix,
|
|
97
|
+
token=config.api_key,
|
|
122
98
|
*args,
|
|
123
99
|
**kwargs,
|
|
124
100
|
)
|
mlops/task/task.py
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"""
|
|
2
|
-
High-level Task SDK interface for
|
|
2
|
+
High-level Task SDK interface for MLOps.
|
|
3
3
|
|
|
4
|
-
This module provides a convenient interface for managing tasks through the
|
|
5
|
-
"""
|
|
4
|
+
This module provides a convenient interface for managing tasks through the MLOps API.
|
|
5
|
+
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
+
import os
|
|
8
9
|
from http import HTTPStatus
|
|
10
|
+
from pathlib import Path
|
|
9
11
|
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
10
15
|
from ..api.client.api.tasks import (
|
|
11
16
|
submit_task,
|
|
12
17
|
get_task,
|
|
@@ -14,8 +19,15 @@ from ..api.client.api.tasks import (
|
|
|
14
19
|
cancel_task,
|
|
15
20
|
delete_task,
|
|
16
21
|
)
|
|
22
|
+
from ..api.client.api.storage import (
|
|
23
|
+
get_storage_presign_upload,
|
|
24
|
+
get_storage_presign_download,
|
|
25
|
+
)
|
|
17
26
|
from ..api.client.models.task import Task as TaskModel
|
|
18
27
|
from ..api.client.models.task_submit_request import TaskSubmitRequest
|
|
28
|
+
from ..api.client.models.task_submit_request_environment_type_0 import (
|
|
29
|
+
TaskSubmitRequestEnvironmentType0,
|
|
30
|
+
)
|
|
19
31
|
from ..api.client.models.task_submit_response import TaskSubmitResponse
|
|
20
32
|
from ..api.client.models.task_list_response import TaskListResponse
|
|
21
33
|
from ..api.client.models.task_status import TaskStatus
|
|
@@ -29,13 +41,46 @@ from ..exceptions import (
|
|
|
29
41
|
from .client import TaskClient, handle_api_exception
|
|
30
42
|
|
|
31
43
|
|
|
44
|
+
def _validate_archive_file_path(file_path: str) -> Path:
|
|
45
|
+
p = Path(os.path.expanduser(file_path)).resolve()
|
|
46
|
+
if not p.exists():
|
|
47
|
+
raise APIException(f"File not found: {p}")
|
|
48
|
+
if not p.is_file():
|
|
49
|
+
raise APIException(f"file_path must be a file: {p}")
|
|
50
|
+
|
|
51
|
+
lower = p.name.lower()
|
|
52
|
+
if not (lower.endswith(".zip") or lower.endswith(".tar.gz") or lower.endswith(".tgz")):
|
|
53
|
+
raise APIException(f"file_path must be one of .zip, .tar.gz, .tgz: {p}")
|
|
54
|
+
return p
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _upload_file_to_presigned_url(url: str, file_path: Path, timeout: Optional[float]) -> None:
|
|
58
|
+
size = file_path.stat().st_size
|
|
59
|
+
# Use a dedicated client for S3 presigned upload (avoid leaking API auth headers).
|
|
60
|
+
with httpx.Client(timeout=timeout) as client:
|
|
61
|
+
with file_path.open("rb") as f:
|
|
62
|
+
resp = client.put(
|
|
63
|
+
url,
|
|
64
|
+
content=f,
|
|
65
|
+
headers={
|
|
66
|
+
"Content-Length": str(size),
|
|
67
|
+
"Content-Type": "application/octet-stream",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
if resp.status_code < 200 or resp.status_code >= 300:
|
|
71
|
+
body = (resp.text or "")[:2048]
|
|
72
|
+
raise APIException(
|
|
73
|
+
f"Failed to upload file to presigned url: HTTP {resp.status_code}: {body}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
32
77
|
class Task:
|
|
33
78
|
"""
|
|
34
79
|
High-level interface for managing tasks.
|
|
35
80
|
|
|
36
81
|
Example:
|
|
37
82
|
```python
|
|
38
|
-
from
|
|
83
|
+
from mlops import Task, ConnectionConfig
|
|
39
84
|
|
|
40
85
|
config = ConnectionConfig(api_key="your_api_key")
|
|
41
86
|
task = Task(config=config)
|
|
@@ -43,28 +88,28 @@ class Task:
|
|
|
43
88
|
# Submit a task with script
|
|
44
89
|
result = task.submit(
|
|
45
90
|
name="my-task",
|
|
46
|
-
|
|
91
|
+
cluster_name="slurm-cn",
|
|
47
92
|
script="#!/bin/bash\\necho 'Hello World'"
|
|
48
93
|
)
|
|
49
94
|
|
|
50
95
|
# Or submit with command
|
|
51
96
|
result = task.submit(
|
|
52
97
|
name="my-task",
|
|
53
|
-
|
|
98
|
+
cluster_name="slurm-cn",
|
|
54
99
|
command="echo 'Hello World'"
|
|
55
100
|
)
|
|
56
101
|
|
|
57
102
|
# Get task details
|
|
58
|
-
task_info = task.get(task_id=result.job_id,
|
|
103
|
+
task_info = task.get(task_id=result.job_id, cluster_name="slurm-cn")
|
|
59
104
|
|
|
60
105
|
# List tasks
|
|
61
106
|
tasks = task.list(status=TaskStatus.RUNNING)
|
|
62
107
|
|
|
63
108
|
# Cancel a task
|
|
64
|
-
task.cancel(task_id=result.job_id,
|
|
109
|
+
task.cancel(task_id=result.job_id, cluster_name="slurm-cn")
|
|
65
110
|
|
|
66
111
|
# Delete a task
|
|
67
|
-
task.delete(task_id=result.job_id,
|
|
112
|
+
task.delete(task_id=result.job_id, cluster_name="slurm-cn")
|
|
68
113
|
```
|
|
69
114
|
"""
|
|
70
115
|
|
|
@@ -72,7 +117,6 @@ class Task:
|
|
|
72
117
|
self,
|
|
73
118
|
config: Optional["ConnectionConfig"] = None,
|
|
74
119
|
api_key: Optional[str] = None,
|
|
75
|
-
access_token: Optional[str] = None,
|
|
76
120
|
domain: Optional[str] = None,
|
|
77
121
|
debug: Optional[bool] = None,
|
|
78
122
|
request_timeout: Optional[float] = None,
|
|
@@ -83,7 +127,6 @@ class Task:
|
|
|
83
127
|
Args:
|
|
84
128
|
config: ConnectionConfig instance. If not provided, a new one will be created.
|
|
85
129
|
api_key: API key for authentication. Overrides config.api_key.
|
|
86
|
-
access_token: Access token for authentication. Overrides config.access_token.
|
|
87
130
|
domain: API domain. Overrides config.domain.
|
|
88
131
|
debug: Enable debug mode. Overrides config.debug.
|
|
89
132
|
request_timeout: Request timeout in seconds. Overrides config.request_timeout.
|
|
@@ -95,8 +138,6 @@ class Task:
|
|
|
95
138
|
# Override config values if provided
|
|
96
139
|
if api_key is not None:
|
|
97
140
|
config.api_key = api_key
|
|
98
|
-
if access_token is not None:
|
|
99
|
-
config.access_token = access_token
|
|
100
141
|
if domain is not None:
|
|
101
142
|
config.domain = domain
|
|
102
143
|
if debug is not None:
|
|
@@ -106,22 +147,22 @@ class Task:
|
|
|
106
147
|
|
|
107
148
|
self._config = config
|
|
108
149
|
self._client = TaskClient(config=config)
|
|
109
|
-
|
|
110
150
|
def submit(
|
|
111
151
|
self,
|
|
112
152
|
name: str,
|
|
113
|
-
|
|
153
|
+
cluster_name: str,
|
|
114
154
|
script: Optional[str] = None,
|
|
115
155
|
command: Optional[str] = None,
|
|
116
156
|
resources: Optional[dict] = None,
|
|
117
157
|
team_id: Optional[int] = None,
|
|
158
|
+
file_path: Optional[str] = None,
|
|
118
159
|
) -> TaskSubmitResponse:
|
|
119
160
|
"""
|
|
120
161
|
Submit a new task.
|
|
121
162
|
|
|
122
163
|
Args:
|
|
123
164
|
name: Task name
|
|
124
|
-
|
|
165
|
+
cluster_name: Cluster name to submit the task to
|
|
125
166
|
script: Task script content (optional, but at least one of script or command is required)
|
|
126
167
|
command: Command to execute (optional, but at least one of script or command is required)
|
|
127
168
|
resources: Resource requirements dict (optional)
|
|
@@ -134,19 +175,16 @@ class Task:
|
|
|
134
175
|
APIException: If the API returns an error
|
|
135
176
|
AuthenticationException: If authentication fails
|
|
136
177
|
"""
|
|
137
|
-
# Validate required fields
|
|
138
|
-
if cluster_id is None:
|
|
139
|
-
raise APIException("cluster_id is required")
|
|
140
|
-
|
|
141
178
|
# At least one of script or command must be provided
|
|
142
179
|
if not script and not command:
|
|
143
180
|
raise APIException("At least one of 'script' or 'command' must be provided")
|
|
144
181
|
|
|
145
182
|
# Map resources dict to individual fields
|
|
146
183
|
# resources dict can contain: cpu, cpus_per_task, memory, nodes, gres, time, partition, etc.
|
|
184
|
+
|
|
147
185
|
request_kwargs = {
|
|
148
186
|
"name": name,
|
|
149
|
-
"
|
|
187
|
+
"cluster_name": cluster_name,
|
|
150
188
|
}
|
|
151
189
|
|
|
152
190
|
# Handle script and command (at least one is required)
|
|
@@ -177,7 +215,87 @@ class Task:
|
|
|
177
215
|
request_kwargs["partition"] = resources.get("partition")
|
|
178
216
|
if "tres" in resources:
|
|
179
217
|
request_kwargs["tres"] = resources.get("tres")
|
|
180
|
-
|
|
218
|
+
|
|
219
|
+
if file_path:
|
|
220
|
+
local_path = _validate_archive_file_path(file_path)
|
|
221
|
+
timeout = self._config.get_request_timeout()
|
|
222
|
+
|
|
223
|
+
# 1) Get presigned upload URL
|
|
224
|
+
presign_upload_obj = get_storage_presign_upload.sync_detailed(
|
|
225
|
+
client=self._client,
|
|
226
|
+
filename=local_path.name,
|
|
227
|
+
)
|
|
228
|
+
presign_upload = presign_upload_obj.parsed
|
|
229
|
+
if isinstance(presign_upload, ErrorResponse):
|
|
230
|
+
status_code = (
|
|
231
|
+
presign_upload.code
|
|
232
|
+
if presign_upload.code != UNSET and presign_upload.code != 0
|
|
233
|
+
else presign_upload_obj.status_code.value
|
|
234
|
+
)
|
|
235
|
+
exception = handle_api_exception(
|
|
236
|
+
Response(
|
|
237
|
+
status_code=HTTPStatus(status_code),
|
|
238
|
+
content=presign_upload_obj.content,
|
|
239
|
+
headers=presign_upload_obj.headers,
|
|
240
|
+
parsed=None,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
raise exception
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
presign_upload is None
|
|
247
|
+
or presign_upload.url in (UNSET, None)
|
|
248
|
+
or presign_upload.key in (UNSET, None)
|
|
249
|
+
):
|
|
250
|
+
raise APIException("Failed to get presigned upload url: empty response")
|
|
251
|
+
|
|
252
|
+
# 2) Upload file to S3 (presigned URL)
|
|
253
|
+
_upload_file_to_presigned_url(
|
|
254
|
+
url=str(presign_upload.url),
|
|
255
|
+
file_path=local_path,
|
|
256
|
+
timeout=timeout,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# 3) Get presigned download URL
|
|
260
|
+
presign_download_obj = get_storage_presign_download.sync_detailed(
|
|
261
|
+
client=self._client,
|
|
262
|
+
key=str(presign_upload.key),
|
|
263
|
+
)
|
|
264
|
+
presign_download = presign_download_obj.parsed
|
|
265
|
+
if isinstance(presign_download, ErrorResponse):
|
|
266
|
+
status_code = (
|
|
267
|
+
presign_download.code
|
|
268
|
+
if presign_download.code != UNSET and presign_download.code != 0
|
|
269
|
+
else presign_download_obj.status_code.value
|
|
270
|
+
)
|
|
271
|
+
exception = handle_api_exception(
|
|
272
|
+
Response(
|
|
273
|
+
status_code=HTTPStatus(status_code),
|
|
274
|
+
content=presign_download_obj.content,
|
|
275
|
+
headers=presign_download_obj.headers,
|
|
276
|
+
parsed=None,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
raise exception
|
|
280
|
+
|
|
281
|
+
if presign_download is None or presign_download.url in (UNSET, None):
|
|
282
|
+
raise APIException(
|
|
283
|
+
"Failed to get presigned download url: empty response"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# 4) Set env var (merge if user already provided environment)
|
|
287
|
+
env: dict[str, str] = {}
|
|
288
|
+
existing_env = request_kwargs.get("environment")
|
|
289
|
+
if isinstance(existing_env, TaskSubmitRequestEnvironmentType0):
|
|
290
|
+
env.update(existing_env.additional_properties)
|
|
291
|
+
elif isinstance(existing_env, dict):
|
|
292
|
+
env.update(existing_env)
|
|
293
|
+
|
|
294
|
+
env["SYSTEM_DOWNLOAD_ARCHIVE_URL"] = str(presign_download.url)
|
|
295
|
+
request_kwargs["environment"] = TaskSubmitRequestEnvironmentType0.from_dict(
|
|
296
|
+
env
|
|
297
|
+
)
|
|
298
|
+
|
|
181
299
|
request = TaskSubmitRequest(**request_kwargs)
|
|
182
300
|
|
|
183
301
|
# Use sync_detailed to get full response information
|
|
@@ -230,14 +348,14 @@ class Task:
|
|
|
230
348
|
def get(
|
|
231
349
|
self,
|
|
232
350
|
task_id: int,
|
|
233
|
-
|
|
351
|
+
cluster_name: str,
|
|
234
352
|
) -> TaskModel:
|
|
235
353
|
"""
|
|
236
354
|
Get task details by task ID.
|
|
237
355
|
|
|
238
356
|
Args:
|
|
239
357
|
task_id: Task ID
|
|
240
|
-
|
|
358
|
+
cluster_name: Cluster name
|
|
241
359
|
|
|
242
360
|
Returns:
|
|
243
361
|
Task model with task details
|
|
@@ -250,7 +368,7 @@ class Task:
|
|
|
250
368
|
response_obj = get_task.sync_detailed(
|
|
251
369
|
id=task_id,
|
|
252
370
|
client=self._client,
|
|
253
|
-
|
|
371
|
+
cluster_name=cluster_name,
|
|
254
372
|
)
|
|
255
373
|
response = response_obj.parsed
|
|
256
374
|
|
|
@@ -302,7 +420,7 @@ class Task:
|
|
|
302
420
|
status: Optional[TaskStatus] = None,
|
|
303
421
|
user_id: Optional[int] = None,
|
|
304
422
|
team_id: Optional[int] = None,
|
|
305
|
-
|
|
423
|
+
cluster_name: Optional[str] = None,
|
|
306
424
|
) -> TaskListResponse:
|
|
307
425
|
"""
|
|
308
426
|
List tasks with optional filtering.
|
|
@@ -313,7 +431,7 @@ class Task:
|
|
|
313
431
|
status: Filter by task status (optional)
|
|
314
432
|
user_id: Filter by user ID (optional)
|
|
315
433
|
team_id: Filter by team ID (optional)
|
|
316
|
-
|
|
434
|
+
cluster_name: Filter by cluster name (optional)
|
|
317
435
|
|
|
318
436
|
Returns:
|
|
319
437
|
TaskListResponse containing the list of tasks
|
|
@@ -329,7 +447,7 @@ class Task:
|
|
|
329
447
|
status=status if status is not None else UNSET,
|
|
330
448
|
user_id=user_id if user_id is not None else UNSET,
|
|
331
449
|
team_id=team_id if team_id is not None else UNSET,
|
|
332
|
-
|
|
450
|
+
cluster_name=cluster_name if cluster_name is not None else UNSET,
|
|
333
451
|
)
|
|
334
452
|
response = response_obj.parsed
|
|
335
453
|
|
|
@@ -377,14 +495,14 @@ class Task:
|
|
|
377
495
|
def cancel(
|
|
378
496
|
self,
|
|
379
497
|
task_id: int,
|
|
380
|
-
|
|
498
|
+
cluster_name: str,
|
|
381
499
|
) -> bool:
|
|
382
500
|
"""
|
|
383
501
|
Cancel a task.
|
|
384
502
|
|
|
385
503
|
Args:
|
|
386
504
|
task_id: Task ID to cancel
|
|
387
|
-
|
|
505
|
+
cluster_name: Cluster name where the task is running
|
|
388
506
|
|
|
389
507
|
Returns:
|
|
390
508
|
True if the task was cancelled successfully
|
|
@@ -397,7 +515,7 @@ class Task:
|
|
|
397
515
|
response_obj = cancel_task.sync_detailed(
|
|
398
516
|
id=task_id,
|
|
399
517
|
client=self._client,
|
|
400
|
-
|
|
518
|
+
cluster_name=cluster_name,
|
|
401
519
|
)
|
|
402
520
|
response = response_obj.parsed
|
|
403
521
|
|
|
@@ -434,14 +552,14 @@ class Task:
|
|
|
434
552
|
def delete(
|
|
435
553
|
self,
|
|
436
554
|
task_id: int,
|
|
437
|
-
|
|
555
|
+
cluster_name: str,
|
|
438
556
|
) -> bool:
|
|
439
557
|
"""
|
|
440
558
|
Delete a task.
|
|
441
559
|
|
|
442
560
|
Args:
|
|
443
561
|
task_id: Task ID to delete
|
|
444
|
-
|
|
562
|
+
cluster_name: Cluster name where the task is running
|
|
445
563
|
|
|
446
564
|
Returns:
|
|
447
565
|
True if the task was deleted successfully
|
|
@@ -454,7 +572,7 @@ class Task:
|
|
|
454
572
|
response_obj = delete_task.sync_detailed(
|
|
455
573
|
id=task_id,
|
|
456
574
|
client=self._client,
|
|
457
|
-
|
|
575
|
+
cluster_name=cluster_name,
|
|
458
576
|
)
|
|
459
577
|
response = response_obj.parsed
|
|
460
578
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: mlops-python-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: MLOps Python SDK for XCloud Service API
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: mlops
|
|
@@ -39,9 +39,9 @@ pip install mlops-python-sdk
|
|
|
39
39
|
|
|
40
40
|
### 1. Setup Authentication
|
|
41
41
|
|
|
42
|
-
You can authenticate using either an API Key
|
|
42
|
+
You can authenticate using either an API Key.
|
|
43
43
|
|
|
44
|
-
####
|
|
44
|
+
#### API Key (Recommended for programmatic access)
|
|
45
45
|
|
|
46
46
|
1. Sign up at [MLOps](https://xcloud-service.com)
|
|
47
47
|
2. Create an API key from [API Keys](https://xcloud-service.com/home/api-keys)
|
|
@@ -52,13 +52,6 @@ export MLOPS_API_KEY=xck_******
|
|
|
52
52
|
export MLOPS_DOMAIN=localhost:8090 # optional, default is localhost:8090
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
#### Option 2: Access Token (For user authentication)
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
export MLOPS_ACCESS_TOKEN=your_access_token
|
|
59
|
-
export MLOPS_DOMAIN=localhost:8090 # optional
|
|
60
|
-
```
|
|
61
|
-
|
|
62
55
|
### 2. Basic Usage
|
|
63
56
|
|
|
64
57
|
```python
|
|
@@ -127,7 +120,6 @@ task = Task()
|
|
|
127
120
|
# With explicit configuration
|
|
128
121
|
config = ConnectionConfig(
|
|
129
122
|
api_key="xck_******", # API key for authentication
|
|
130
|
-
access_token="token_******", # Access token (alternative to API key)
|
|
131
123
|
domain="localhost:8090", # API domain
|
|
132
124
|
debug=False, # Enable debug mode
|
|
133
125
|
request_timeout=30.0 # Request timeout in seconds
|
|
@@ -280,7 +272,6 @@ TaskStatus.CREATED # Task was created
|
|
|
280
272
|
The SDK reads configuration from environment variables:
|
|
281
273
|
|
|
282
274
|
- `MLOPS_API_KEY`: API key for authentication
|
|
283
|
-
- `MLOPS_ACCESS_TOKEN`: Access token for authentication (alternative to API key)
|
|
284
275
|
- `MLOPS_DOMAIN`: API domain (default: `localhost:8090`)
|
|
285
276
|
- `MLOPS_DEBUG`: Enable debug mode (`true`/`false`, default: `false`)
|
|
286
277
|
- `MLOPS_API_PATH`: API path prefix (default: `/api/v1`)
|