mlops-python-sdk 1.0.3__py3-none-any.whl → 1.0.4__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/connection_config.py +17 -12
- mlops/task/task.py +144 -70
- {mlops_python_sdk-1.0.3.dist-info → mlops_python_sdk-1.0.4.dist-info}/METADATA +1 -1
- {mlops_python_sdk-1.0.3.dist-info → mlops_python_sdk-1.0.4.dist-info}/RECORD +5 -5
- {mlops_python_sdk-1.0.3.dist-info → mlops_python_sdk-1.0.4.dist-info}/WHEEL +0 -0
mlops/connection_config.py
CHANGED
|
@@ -66,17 +66,7 @@ class ConnectionConfig:
|
|
|
66
66
|
self.api_path = "/" + self.api_path
|
|
67
67
|
|
|
68
68
|
# Build API URL
|
|
69
|
-
|
|
70
|
-
base_url = "http://localhost:8090"
|
|
71
|
-
else:
|
|
72
|
-
# If domain already includes protocol, use it as-is
|
|
73
|
-
# Otherwise, default to http:// for backward compatibility
|
|
74
|
-
if self.domain.startswith(("http://", "https://")):
|
|
75
|
-
base_url = self.domain
|
|
76
|
-
else:
|
|
77
|
-
base_url = f"http://{self.domain}"
|
|
78
|
-
|
|
79
|
-
self.api_url = f"{base_url}{self.api_path}"
|
|
69
|
+
self.build_api_url()
|
|
80
70
|
|
|
81
71
|
@staticmethod
|
|
82
72
|
def _get_request_timeout(
|
|
@@ -89,7 +79,22 @@ class ConnectionConfig:
|
|
|
89
79
|
return request_timeout
|
|
90
80
|
else:
|
|
91
81
|
return default_timeout
|
|
92
|
-
|
|
82
|
+
|
|
83
|
+
def build_api_url(self) -> None:
|
|
84
|
+
if self.debug:
|
|
85
|
+
base_url = "http://localhost:8090"
|
|
86
|
+
else:
|
|
87
|
+
# If domain already includes protocol, use it as-is
|
|
88
|
+
# Otherwise, default to http:// for backward compatibility
|
|
89
|
+
if self.domain.startswith(("http://", "https://")):
|
|
90
|
+
base_url = self.domain
|
|
91
|
+
elif self.domain.startswith("localhost") or self.domain.startswith("127.0.0.1"):
|
|
92
|
+
base_url = f"http://{self.domain}"
|
|
93
|
+
else:
|
|
94
|
+
base_url = f"https://{self.domain}"
|
|
95
|
+
|
|
96
|
+
self.api_url = f"{base_url}{self.api_path}"
|
|
97
|
+
|
|
93
98
|
def get_request_timeout(self, request_timeout: Optional[float] = None):
|
|
94
99
|
return self._get_request_timeout(self.request_timeout, request_timeout)
|
|
95
100
|
|
mlops/task/task.py
CHANGED
|
@@ -6,12 +6,15 @@ This module provides a convenient interface for managing tasks through the MLOps
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import shutil
|
|
9
10
|
import sys
|
|
10
11
|
import threading
|
|
12
|
+
import tempfile
|
|
11
13
|
import time
|
|
14
|
+
import zipfile
|
|
12
15
|
from http import HTTPStatus
|
|
13
16
|
from pathlib import Path
|
|
14
|
-
from typing import Optional
|
|
17
|
+
from typing import Callable, Optional
|
|
15
18
|
|
|
16
19
|
import httpx
|
|
17
20
|
|
|
@@ -56,6 +59,78 @@ def _validate_archive_file_path(file_path: str) -> Path:
|
|
|
56
59
|
raise APIException(f"file_path must be one of .zip, .tar.gz, .tgz: {p}")
|
|
57
60
|
return p
|
|
58
61
|
|
|
62
|
+
def _is_archive_path(p: Path) -> bool:
|
|
63
|
+
lower = p.name.lower()
|
|
64
|
+
return lower.endswith(".zip") or lower.endswith(".tar.gz") or lower.endswith(".tgz")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _zip_directory(src_dir: Path, dst_zip: Path) -> None:
|
|
68
|
+
src_dir = src_dir.resolve()
|
|
69
|
+
root_name = src_dir.name
|
|
70
|
+
with zipfile.ZipFile(dst_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
71
|
+
for p in src_dir.rglob("*"):
|
|
72
|
+
if p.is_dir():
|
|
73
|
+
continue
|
|
74
|
+
rel = p.relative_to(src_dir).as_posix()
|
|
75
|
+
if rel in ("", "."):
|
|
76
|
+
continue
|
|
77
|
+
zf.write(p, arcname=f"{root_name}/{rel}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _zip_file(src_file: Path, dst_zip: Path) -> None:
|
|
81
|
+
src_file = src_file.resolve()
|
|
82
|
+
with zipfile.ZipFile(dst_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
83
|
+
zf.write(src_file, arcname=src_file.name)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _path_to_archive_path(task_name: str, file_path: str) -> tuple[Path, Callable[[], None]]:
|
|
87
|
+
"""
|
|
88
|
+
Mirror cli-go `pathToArchivePath` behavior:
|
|
89
|
+
- directory: zip it
|
|
90
|
+
- file: if already an archive (.zip/.tar.gz/.tgz) return as-is; otherwise zip it
|
|
91
|
+
Returns (archive_path, cleanup_callable).
|
|
92
|
+
"""
|
|
93
|
+
p = Path(os.path.expanduser(file_path)).resolve()
|
|
94
|
+
if not p.exists():
|
|
95
|
+
raise APIException(f"File not found: {p}")
|
|
96
|
+
|
|
97
|
+
if p.is_dir():
|
|
98
|
+
tmp_dir = (
|
|
99
|
+
Path(tempfile.gettempdir())
|
|
100
|
+
/ "xservice-cli"
|
|
101
|
+
/ task_name
|
|
102
|
+
/ time.strftime("%Y%m%d%H%M%S")
|
|
103
|
+
)
|
|
104
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
archive_path = tmp_dir / f"{p.name}.zip"
|
|
106
|
+
try:
|
|
107
|
+
_zip_directory(p, archive_path)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
110
|
+
raise APIException(f"failed to compress directory: {e}") from e
|
|
111
|
+
return archive_path, lambda: shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
112
|
+
|
|
113
|
+
if not p.is_file():
|
|
114
|
+
raise APIException(f"file_path must be a file or directory: {p}")
|
|
115
|
+
|
|
116
|
+
if _is_archive_path(p):
|
|
117
|
+
return _validate_archive_file_path(str(p)), lambda: None
|
|
118
|
+
|
|
119
|
+
tmp_dir = (
|
|
120
|
+
Path(tempfile.gettempdir())
|
|
121
|
+
/ "xservice-cli"
|
|
122
|
+
/ task_name
|
|
123
|
+
/ time.strftime("%Y%m%d%H%M%S")
|
|
124
|
+
)
|
|
125
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
archive_path = tmp_dir / f"{p.name}.zip"
|
|
127
|
+
try:
|
|
128
|
+
_zip_file(p, archive_path)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
131
|
+
raise APIException(f"failed to compress file: {e}") from e
|
|
132
|
+
return archive_path, lambda: shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
133
|
+
|
|
59
134
|
|
|
60
135
|
def _upload_file_to_presigned_url(url: str, file_path: Path, timeout: Optional[float]) -> None:
|
|
61
136
|
def _format_bytes_iec(n: int) -> str:
|
|
@@ -246,6 +321,7 @@ class Task:
|
|
|
246
321
|
config.api_key = api_key
|
|
247
322
|
if domain is not None:
|
|
248
323
|
config.domain = domain
|
|
324
|
+
config.build_api_url()
|
|
249
325
|
if debug is not None:
|
|
250
326
|
config.debug = debug
|
|
251
327
|
if request_timeout is not None:
|
|
@@ -328,84 +404,82 @@ class Task:
|
|
|
328
404
|
request_kwargs["ntasks"] = 1
|
|
329
405
|
|
|
330
406
|
if file_path:
|
|
331
|
-
local_path =
|
|
407
|
+
local_path, cleanup = _path_to_archive_path(name, file_path)
|
|
332
408
|
timeout = self._config.get_request_timeout()
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
presign_upload = presign_upload_obj.parsed
|
|
340
|
-
if isinstance(presign_upload, ErrorResponse):
|
|
341
|
-
status_code = (
|
|
342
|
-
presign_upload.code
|
|
343
|
-
if presign_upload.code != UNSET and presign_upload.code != 0
|
|
344
|
-
else presign_upload_obj.status_code.value
|
|
409
|
+
try:
|
|
410
|
+
# 1) Get presigned upload URL
|
|
411
|
+
presign_upload_obj = get_storage_presign_upload.sync_detailed(
|
|
412
|
+
client=self._client,
|
|
413
|
+
filename=local_path.name,
|
|
345
414
|
)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
415
|
+
presign_upload = presign_upload_obj.parsed
|
|
416
|
+
if isinstance(presign_upload, ErrorResponse):
|
|
417
|
+
status_code = (
|
|
418
|
+
presign_upload.code
|
|
419
|
+
if presign_upload.code != UNSET and presign_upload.code != 0
|
|
420
|
+
else presign_upload_obj.status_code.value
|
|
421
|
+
)
|
|
422
|
+
exception = handle_api_exception(
|
|
423
|
+
Response(
|
|
424
|
+
status_code=HTTPStatus(status_code),
|
|
425
|
+
content=presign_upload_obj.content,
|
|
426
|
+
headers=presign_upload_obj.headers,
|
|
427
|
+
parsed=None,
|
|
428
|
+
)
|
|
352
429
|
)
|
|
430
|
+
raise exception
|
|
431
|
+
|
|
432
|
+
if (
|
|
433
|
+
presign_upload is None
|
|
434
|
+
or presign_upload.url in (UNSET, None)
|
|
435
|
+
or presign_upload.key in (UNSET, None)
|
|
436
|
+
):
|
|
437
|
+
raise APIException("Failed to get presigned upload url: empty response")
|
|
438
|
+
|
|
439
|
+
# 2) Upload file to S3 (presigned URL)
|
|
440
|
+
_upload_file_to_presigned_url(
|
|
441
|
+
url=str(presign_upload.url),
|
|
442
|
+
file_path=local_path,
|
|
443
|
+
timeout=timeout,
|
|
353
444
|
)
|
|
354
|
-
raise exception
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
presign_upload is None
|
|
358
|
-
or presign_upload.url in (UNSET, None)
|
|
359
|
-
or presign_upload.key in (UNSET, None)
|
|
360
|
-
):
|
|
361
|
-
raise APIException("Failed to get presigned upload url: empty response")
|
|
362
|
-
|
|
363
|
-
# 2) Upload file to S3 (presigned URL)
|
|
364
|
-
_upload_file_to_presigned_url(
|
|
365
|
-
url=str(presign_upload.url),
|
|
366
|
-
file_path=local_path,
|
|
367
|
-
timeout=timeout,
|
|
368
|
-
)
|
|
369
445
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
)
|
|
375
|
-
presign_download = presign_download_obj.parsed
|
|
376
|
-
if isinstance(presign_download, ErrorResponse):
|
|
377
|
-
status_code = (
|
|
378
|
-
presign_download.code
|
|
379
|
-
if presign_download.code != UNSET and presign_download.code != 0
|
|
380
|
-
else presign_download_obj.status_code.value
|
|
446
|
+
# 3) Get presigned download URL
|
|
447
|
+
presign_download_obj = get_storage_presign_download.sync_detailed(
|
|
448
|
+
client=self._client,
|
|
449
|
+
key=str(presign_upload.key),
|
|
381
450
|
)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
451
|
+
presign_download = presign_download_obj.parsed
|
|
452
|
+
if isinstance(presign_download, ErrorResponse):
|
|
453
|
+
status_code = (
|
|
454
|
+
presign_download.code
|
|
455
|
+
if presign_download.code != UNSET and presign_download.code != 0
|
|
456
|
+
else presign_download_obj.status_code.value
|
|
388
457
|
)
|
|
389
|
-
|
|
390
|
-
|
|
458
|
+
exception = handle_api_exception(
|
|
459
|
+
Response(
|
|
460
|
+
status_code=HTTPStatus(status_code),
|
|
461
|
+
content=presign_download_obj.content,
|
|
462
|
+
headers=presign_download_obj.headers,
|
|
463
|
+
parsed=None,
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
raise exception
|
|
391
467
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
"Failed to get presigned download url: empty response"
|
|
395
|
-
)
|
|
468
|
+
if presign_download is None or presign_download.url in (UNSET, None):
|
|
469
|
+
raise APIException("Failed to get presigned download url: empty response")
|
|
396
470
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
471
|
+
# 4) Set env var (merge if user already provided environment)
|
|
472
|
+
env: dict[str, str] = {}
|
|
473
|
+
existing_env = request_kwargs.get("environment")
|
|
474
|
+
if isinstance(existing_env, TaskSubmitRequestEnvironmentType0):
|
|
475
|
+
env.update(existing_env.additional_properties)
|
|
476
|
+
elif isinstance(existing_env, dict):
|
|
477
|
+
env.update(existing_env)
|
|
478
|
+
|
|
479
|
+
env["SYSTEM_DOWNLOAD_ARCHIVE_URL"] = str(presign_download.url)
|
|
480
|
+
request_kwargs["environment"] = TaskSubmitRequestEnvironmentType0.from_dict(env)
|
|
481
|
+
finally:
|
|
482
|
+
cleanup()
|
|
409
483
|
|
|
410
484
|
request = TaskSubmitRequest(**request_kwargs)
|
|
411
485
|
|
|
@@ -42,11 +42,11 @@ mlops/api/client/models/task_tres_type_0.py,sha256=rEaiQG7A19mlTIHDppzxuWa4oPfh9
|
|
|
42
42
|
mlops/api/client/models/task_tres_used_type_0.py,sha256=4w6An7-ZCqa8cc3SPi7mcwGK-ekT6AYq_dEdf8KzoYA,1320
|
|
43
43
|
mlops/api/client/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
|
|
44
44
|
mlops/api/client/types.py,sha256=AX4orxQZQJat3vZrgjJ-TYb2sNBL8kNo9yqYDT-n8y8,1391
|
|
45
|
-
mlops/connection_config.py,sha256=
|
|
45
|
+
mlops/connection_config.py,sha256=yrY-FKyqtgqXmbAQyhlLIwDy1wDyjnT_mOhAFHAzek0,3170
|
|
46
46
|
mlops/exceptions.py,sha256=3kfda-Rz0km9kV-gvnPCw7ueemWkXIGGdT0NXx6z9Xk,1680
|
|
47
47
|
mlops/task/__init__.py,sha256=M983vMPLj3tZQNFXQyTP5I2RsRorFElezLeppr3WLsw,133
|
|
48
48
|
mlops/task/client.py,sha256=V131WLVJl1raGAVixUhJCX8s1neN15mxAjQwO01qlIg,3552
|
|
49
|
-
mlops/task/task.py,sha256=
|
|
50
|
-
mlops_python_sdk-1.0.
|
|
51
|
-
mlops_python_sdk-1.0.
|
|
52
|
-
mlops_python_sdk-1.0.
|
|
49
|
+
mlops/task/task.py,sha256=HT7TtOqLw4FvF80c-_I-XWK97_9OBR7pC2i2NGZNVO4,30663
|
|
50
|
+
mlops_python_sdk-1.0.4.dist-info/METADATA,sha256=3_g9WfaGdDtmNBnqMONGOTL_Mol8UAdC607tE947kLs,5679
|
|
51
|
+
mlops_python_sdk-1.0.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
52
|
+
mlops_python_sdk-1.0.4.dist-info/RECORD,,
|
|
File without changes
|