fiddler-client 2.3.0.dev2__py3-none-any.whl → 2.4.0.dev1__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.
Files changed (65) hide show
  1. fiddler/__init__.py +2 -0
  2. fiddler/_version.py +1 -1
  3. fiddler/api/alert_mixin.py +1 -1
  4. fiddler/api/api.py +2 -0
  5. fiddler/api/webhooks_mixin.py +9 -7
  6. fiddler/core_objects.py +98 -47
  7. fiddler/schemas/custom_features.py +25 -19
  8. fiddler3/constants/baseline.py +19 -0
  9. fiddler3/constants/common.py +12 -0
  10. fiddler3/constants/dataset.py +7 -0
  11. fiddler3/constants/model_deployment.py +15 -0
  12. fiddler3/constants/xai.py +11 -0
  13. fiddler3/entities/__init__.py +5 -1
  14. fiddler3/entities/base.py +22 -18
  15. fiddler3/entities/baseline.py +228 -0
  16. fiddler3/entities/dataset.py +149 -0
  17. fiddler3/entities/job.py +9 -8
  18. fiddler3/entities/model.py +160 -29
  19. fiddler3/entities/model_artifact.py +350 -0
  20. fiddler3/entities/model_deployment.py +112 -0
  21. fiddler3/entities/model_surrogate.py +92 -0
  22. fiddler3/entities/organization.py +38 -0
  23. fiddler3/entities/project.py +69 -8
  24. fiddler3/entities/user.py +45 -0
  25. fiddler3/entities/xai.py +309 -0
  26. fiddler3/libs/http_client.py +24 -13
  27. fiddler3/libs/json_encoder.py +12 -0
  28. fiddler3/schemas/__init__.py +4 -0
  29. fiddler3/schemas/baseline.py +37 -0
  30. fiddler3/schemas/custom_features.py +23 -18
  31. fiddler3/schemas/dataset.py +27 -0
  32. fiddler3/schemas/deployment_params.py +25 -0
  33. fiddler3/schemas/filter_query.py +54 -0
  34. fiddler3/schemas/job.py +2 -2
  35. fiddler3/schemas/model.py +9 -9
  36. fiddler3/schemas/model_artifact.py +6 -0
  37. fiddler3/schemas/model_deployment.py +26 -0
  38. fiddler3/schemas/organization.py +9 -1
  39. fiddler3/schemas/project.py +4 -4
  40. fiddler3/schemas/server_info.py +2 -2
  41. fiddler3/schemas/user.py +1 -1
  42. fiddler3/schemas/xai.py +32 -0
  43. fiddler3/tests/apis/test_baseline.py +292 -0
  44. fiddler3/tests/apis/test_dataset.py +173 -0
  45. fiddler3/tests/apis/test_mixin.py +77 -0
  46. fiddler3/tests/apis/test_model.py +102 -7
  47. fiddler3/tests/apis/test_model_artifact.py +175 -0
  48. fiddler3/tests/apis/test_model_deployment.py +98 -0
  49. fiddler3/tests/apis/test_model_surrogate.py +159 -0
  50. fiddler3/tests/apis/test_project.py +36 -20
  51. fiddler3/tests/apis/test_xai.py +690 -0
  52. fiddler3/tests/constants.py +10 -0
  53. fiddler3/tests/test_json_encoder.py +16 -0
  54. fiddler3/tests/test_logger.py +12 -0
  55. fiddler3/tests/test_utils.py +19 -0
  56. fiddler3/utils/__init__.py +1 -0
  57. fiddler3/utils/helpers.py +43 -0
  58. fiddler3/utils/logger.py +15 -0
  59. fiddler3/utils/validations.py +11 -0
  60. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/METADATA +7 -5
  61. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/RECORD +65 -33
  62. tests/fiddler/test_segments.py +227 -0
  63. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/LICENSE.txt +0 -0
  64. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/WHEEL +0 -0
  65. {fiddler_client-2.3.0.dev2.dist-info → fiddler_client-2.4.0.dev1.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import builtins
4
+ import typing
5
+ from dataclasses import dataclass
3
6
  from datetime import datetime
4
7
  from pathlib import Path
5
- from typing import Any
8
+ from typing import Any, Iterator
6
9
  from uuid import UUID
7
10
 
8
11
  import pandas as pd
@@ -12,20 +15,34 @@ from fiddler3.decorators import handle_api_error
12
15
  from fiddler3.entities.base import BaseEntity
13
16
  from fiddler3.entities.helpers import raise_not_found
14
17
  from fiddler3.entities.job import Job
15
- from fiddler3.schemas.job import JobCompact
16
- from fiddler3.schemas.model import ModelResponse
18
+ from fiddler3.entities.model_artifact import ArtifactMixin
19
+ from fiddler3.entities.model_surrogate import SurrogateMixin
20
+ from fiddler3.entities.project import ProjectCompactMixin
21
+ from fiddler3.entities.user import CreatedByMixin, UpdatedByMixin
22
+ from fiddler3.entities.xai import XaiMixin
23
+ from fiddler3.schemas.filter_query import OperatorType, QueryCondition, QueryRule
24
+ from fiddler3.schemas.job import JobCompactResp
25
+ from fiddler3.schemas.model import ModelResp
17
26
  from fiddler3.schemas.model_schema import Column, ModelSchema
18
27
  from fiddler3.schemas.model_spec import ModelSpec
19
28
  from fiddler3.schemas.model_task_params import ModelTaskParams
20
29
  from fiddler3.schemas.xai_params import XaiParams
21
- from fiddler3.utils.logger import get_logger
22
30
  from fiddler3.utils.model_schema_generator import SchemaGeneratorFactory
23
31
 
24
- logger = get_logger(__name__)
32
+ if typing.TYPE_CHECKING:
33
+ from fiddler3.entities.dataset import Dataset
25
34
 
26
35
 
27
- class Model(BaseEntity):
28
- def __init__(
36
+ class Model(
37
+ BaseEntity,
38
+ ArtifactMixin,
39
+ CreatedByMixin,
40
+ ProjectCompactMixin,
41
+ UpdatedByMixin,
42
+ SurrogateMixin,
43
+ XaiMixin,
44
+ ): # pylint: disable=too-many-ancestors
45
+ def __init__( # pylint: disable=too-many-arguments
29
46
  self,
30
47
  name: str,
31
48
  project_id: UUID,
@@ -40,9 +57,7 @@ class Model(BaseEntity):
40
57
  event_ts_format: str | None = None,
41
58
  xai_params: XaiParams | None = None,
42
59
  ) -> None:
43
- """
44
- Construct a model instance
45
- """
60
+ """Construct a model instance"""
46
61
  self.name = name
47
62
  self.project_id = project_id
48
63
  self.schema = schema
@@ -69,20 +84,20 @@ class Model(BaseEntity):
69
84
  self.updated_at: datetime | None = None
70
85
 
71
86
  # Deserialized response object
72
- self._resp: ModelResponse | None = None
87
+ self._resp: ModelResp | None = None
73
88
 
74
89
  @staticmethod
75
90
  def _get_url(id_: UUID | str | None = None) -> str:
76
- """Get model resource/item url"""
91
+ """Get model resource/item url."""
77
92
  url = '/v3/models'
78
93
  return url if not id_ else f'{url}/{id_}'
79
94
 
80
95
  @classmethod
81
96
  def _from_dict(cls, data: dict) -> Model:
82
- """Build entity object from the given dictionary"""
97
+ """Build entity object from the given dictionary."""
83
98
 
84
99
  # Deserialize the response
85
- resp_obj = ModelResponse(**data)
100
+ resp_obj = ModelResp(**data)
86
101
 
87
102
  # Initialize
88
103
  instance = cls(
@@ -120,21 +135,69 @@ class Model(BaseEntity):
120
135
  instance._resp = resp_obj
121
136
  return instance
122
137
 
138
+ def _refresh(self, data: dict) -> None:
139
+ """Refresh the fields of this instance from the given response dictionary"""
140
+ # Deserialize the response
141
+ resp_obj = ModelResp(**data)
142
+
143
+ # Reset fields
144
+ self.schema = resp_obj.schema_
145
+ self.project_id = resp_obj.project.id
146
+
147
+ fields = [
148
+ 'id',
149
+ 'name',
150
+ 'spec',
151
+ 'input_type',
152
+ 'task',
153
+ 'task_params',
154
+ 'description',
155
+ 'event_id_col',
156
+ 'event_ts_col',
157
+ 'event_ts_format',
158
+ 'xai_params',
159
+ 'created_at',
160
+ 'updated_at',
161
+ 'artifact_status',
162
+ 'artifact_files',
163
+ 'input_cols',
164
+ 'output_cols',
165
+ 'target_cols',
166
+ 'metadata_cols',
167
+ 'decision_cols',
168
+ 'is_binary_ranking_model',
169
+ ]
170
+ for field in fields:
171
+ setattr(self, field, getattr(resp_obj, field, None))
172
+
173
+ self._resp = resp_obj
174
+
123
175
  @classmethod
124
176
  @handle_api_error
125
177
  def get(cls, id_: UUID | str) -> Model:
126
- """Get the model instance using model id"""
178
+ """Get the model instance using model id."""
127
179
  response = cls._client().get(url=cls._get_url(id_))
128
180
  return cls._from_response(response=response)
129
181
 
130
182
  @classmethod
131
183
  @handle_api_error
132
- def from_name(cls, model_name: str, project_name: str) -> Model:
133
- """Get the model instance using model name and project name"""
184
+ def from_name(cls, name: str, project_name: str) -> Model:
185
+ """Get the model instance using model name and project name."""
186
+ _filter = QueryCondition(
187
+ rules=[
188
+ QueryRule(field='name', operator=OperatorType.EQUAL, value=name),
189
+ QueryRule(
190
+ field='project_name',
191
+ operator=OperatorType.EQUAL,
192
+ value=project_name,
193
+ ),
194
+ ]
195
+ )
196
+
134
197
  response = cls._client().get(
135
- url=cls._get_url(),
136
- params={'name': model_name, 'project_name': project_name},
198
+ url=cls._get_url(), params={'filter': _filter.json()}
137
199
  )
200
+
138
201
  if response.json()['data']['total'] == 0:
139
202
  raise_not_found('Model not found for the given identifier')
140
203
 
@@ -142,7 +205,7 @@ class Model(BaseEntity):
142
205
 
143
206
  @handle_api_error
144
207
  def create(self) -> Model:
145
- """Create a new model"""
208
+ """Create a new model."""
146
209
  response = self._client().post(
147
210
  url=self._get_url(),
148
211
  data={
@@ -160,24 +223,68 @@ class Model(BaseEntity):
160
223
  'xai_params': self.xai_params.dict(),
161
224
  },
162
225
  )
163
- return self._from_response(response=response)
226
+ self._refresh_from_response(response=response)
227
+ return self
228
+
229
+ @handle_api_error
230
+ def update(self) -> None:
231
+ """Update an existing model."""
232
+ body: dict[str, Any] = {
233
+ 'xai_params': self.xai_params.dict(),
234
+ 'description': self.description,
235
+ 'event_id_col': self.event_id_col,
236
+ 'event_ts_col': self.event_ts_col,
237
+ 'event_ts_format': self.event_ts_format,
238
+ }
239
+
240
+ response = self._client().patch(url=self._get_url(id_=self.id), data=body)
241
+ self._refresh_from_response(response=response)
242
+
243
+ @classmethod
244
+ @handle_api_error
245
+ def list(
246
+ cls,
247
+ project_id: UUID | None = None,
248
+ ) -> Iterator[ModelCompact]:
249
+ """
250
+ Get a list of all models with the given filters
251
+
252
+ :param project_id: Project identifier
253
+ :return: ModelCompact iterator
254
+ """
255
+ params = {}
256
+ if project_id:
257
+ params['project_id'] = project_id
258
+
259
+ for model in cls._paginate(url=cls._get_url(), params=params):
260
+ yield ModelCompact(id=model['id'], name=model['name'])
261
+
262
+ @property
263
+ def datasets(self) -> Iterator[Dataset]:
264
+ """Fetch all the datasets of this model"""
265
+ from fiddler3.entities.dataset import ( # pylint: disable=import-outside-toplevel
266
+ Dataset,
267
+ )
268
+
269
+ yield from Dataset.list(model_id=self.id)
164
270
 
165
271
  @classmethod
166
272
  @handle_api_error
167
273
  def generate_schema(
168
274
  cls,
169
- source: pd.DataFrame | Path | list[dict[str, Any]] | str,
275
+ source: pd.DataFrame | Path | builtins.list[dict[str, Any]] | str,
170
276
  max_cardinality: int | None = None,
171
277
  sample_size: int | None = None,
172
278
  enrich: bool = True,
173
279
  ) -> ModelSchema:
174
280
  """
175
- Generate model schema from the given data
281
+ Generate model schema from the given data.
282
+
176
283
  :param source: Data source - Dataframe or path to csv or parquet file.
177
- :param max_cardinality: Max cardinality to detect categorical columns
178
- :param sample_size: No. of samples to use for generating schema
179
- :param enrich: Enrich the model schema client side by scanning all data
180
- :return: Generated ModelSchema object
284
+ :param max_cardinality: Max cardinality to detect categorical columns.
285
+ :param sample_size: No. of samples to use for generating schema.
286
+ :param enrich: Enrich the model schema client side by scanning all data.
287
+ :return: Generated ModelSchema object.
181
288
  """
182
289
  schema_generator = SchemaGeneratorFactory.create(
183
290
  source=source,
@@ -190,9 +297,33 @@ class Model(BaseEntity):
190
297
 
191
298
  @handle_api_error
192
299
  def delete(self) -> Job:
193
- """Delete a model and it's associated resources"""
300
+ """Delete a model and it's associated resources."""
194
301
  assert self.id is not None
195
302
  response = self._client().delete(url=self._get_url(id_=self.id))
196
303
 
197
- job_compact = JobCompact(**response.json()['data']['job'])
304
+ job_compact = JobCompactResp(**response.json()['data']['job'])
198
305
  return Job.get(id_=job_compact.id)
306
+
307
+
308
+ @dataclass
309
+ class ModelCompact:
310
+ id: UUID
311
+ name: str
312
+
313
+ def fetch(self) -> Model:
314
+ """Fetch model instance"""
315
+ return Model.get(id_=self.id)
316
+
317
+
318
+ class ModelCompactMixin:
319
+ @property
320
+ def model(self) -> ModelCompact:
321
+ """Model instance"""
322
+ response = getattr(self, '_resp', None)
323
+ if not response or not hasattr(response, 'model'):
324
+ raise AttributeError(
325
+ 'This property is available only for objects generated from API '
326
+ 'response.'
327
+ )
328
+
329
+ return ModelCompact(id=response.model.id, name=response.model.name)
@@ -0,0 +1,350 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import math
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+ from uuid import UUID
11
+
12
+ from fiddler3.constants.common import (
13
+ CONTENT_TYPE_OCTET_STREAM_HEADER,
14
+ MULTI_PART_CHUNK_SIZE,
15
+ )
16
+ from fiddler3.decorators import handle_api_error
17
+ from fiddler3.entities.job import Job
18
+ from fiddler3.schemas.deployment_params import ArtifactType, DeploymentParams
19
+ from fiddler3.schemas.job import JobCompactResp
20
+ from fiddler3.schemas.model_artifact import ModelArtifactDeployMultiPartUploadResp
21
+ from fiddler3.utils.logger import get_logger
22
+ from fiddler3.utils.validations import validate_artifact_dir
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ class ArtifactMixin:
28
+ id: UUID | None
29
+ _client: Callable
30
+
31
+ def _get_method(self, update: bool = False) -> Callable:
32
+ """Get HTTP method"""
33
+ return self._client().put if update else self._client().post
34
+
35
+ def _deploy_model_artifact(
36
+ self,
37
+ model_dir: str | Path,
38
+ deployment_params: DeploymentParams | None = None,
39
+ update: bool = False,
40
+ ) -> Job:
41
+ """
42
+ Upload and deploy model artifact for an existing model
43
+
44
+ :param model_dir: Model artifact directory
45
+ :param deployment_params: Model deployment parameters
46
+ :param update: Set True for updating artifact, False for adding artifact
47
+ :return: Async job
48
+ """
49
+ model_dir = Path(model_dir)
50
+ validate_artifact_dir(model_dir)
51
+
52
+ if (
53
+ deployment_params
54
+ and deployment_params.artifact_type.upper() == ArtifactType.SURROGATE
55
+ ):
56
+ raise ValueError(
57
+ f'{ArtifactType.SURROGATE} artifact_type is an invalid value for this '
58
+ f'method. Use {ArtifactType.PYTHON_PACKAGE} instead.'
59
+ )
60
+
61
+ with tempfile.TemporaryDirectory() as tmp:
62
+ # Archive model artifact directory
63
+ logger.info(
64
+ 'Model[%s] - Tarring model artifact directory - %s',
65
+ self.id,
66
+ model_dir,
67
+ )
68
+ file_path = shutil.make_archive(
69
+ base_name=str(Path(tmp) / 'files'),
70
+ format='tar',
71
+ root_dir=str(model_dir),
72
+ base_dir='.',
73
+ )
74
+
75
+ logger.info(
76
+ 'Model[%s] - Model artifact tar file created at %s',
77
+ self.id,
78
+ file_path,
79
+ )
80
+
81
+ # Choose deployer based on archive file size
82
+ if os.path.getsize(file_path) < MULTI_PART_CHUNK_SIZE:
83
+ job = self._artifact_deploy(
84
+ file_path=Path(file_path),
85
+ deployment_params=deployment_params,
86
+ update=update,
87
+ )
88
+ else:
89
+ job = self._artifact_deploy_multipart(
90
+ file_path=Path(file_path),
91
+ deployment_params=deployment_params,
92
+ update=update,
93
+ )
94
+
95
+ logger.info(
96
+ 'Model[%s] - Submitted job (%s) for deploying model artifact',
97
+ self.id,
98
+ job.id,
99
+ )
100
+
101
+ return job
102
+
103
+ def _initialize_multi_part_upload(self, update: bool = False) -> str:
104
+ """
105
+ Initialize multi-part upload request
106
+
107
+ :return: Multi-part upload id
108
+ """
109
+ logger.info(
110
+ 'Model[%s] - Initializing multi-part model upload',
111
+ self.id,
112
+ )
113
+
114
+ method = self._get_method(update)
115
+
116
+ response = method(url=f'/v3/models/{self.id}/deploy-artifact-multi-part-init')
117
+
118
+ response_data = response.json().get('data', {})
119
+
120
+ logger.info(
121
+ 'Model[%s] - Multi-part model upload initialized',
122
+ self.id,
123
+ )
124
+ return response_data.get('upload_id', '')
125
+
126
+ def _upload_multi_part_chunk(
127
+ self, data: bytes, upload_id: str, part_number: int, update: bool
128
+ ) -> dict:
129
+ """
130
+ Upload data chunk
131
+
132
+ :param data: Data chunk
133
+ :param upload_id: Multi-part upload id
134
+ :param part_number: Chunk part number
135
+ :return: Part details
136
+ """
137
+ method = self._get_method(update)
138
+
139
+ response = method(
140
+ url=f'/v3/models/{self.id}/deploy-artifact-multi-part-upload',
141
+ params={
142
+ 'upload_id': upload_id,
143
+ 'part_number': part_number,
144
+ },
145
+ data=data,
146
+ headers=CONTENT_TYPE_OCTET_STREAM_HEADER,
147
+ )
148
+ response_data = ModelArtifactDeployMultiPartUploadResp(
149
+ **response.json()['data']
150
+ )
151
+
152
+ return {
153
+ 'etag': response_data.etag,
154
+ 'part_number': response_data.part_number,
155
+ }
156
+
157
+ def _complete_multi_part_upload(
158
+ self,
159
+ upload_id: str,
160
+ parts: list[dict],
161
+ update: bool,
162
+ deployment_params: DeploymentParams | None = None,
163
+ ) -> Job:
164
+ """
165
+ Complete multi-part upload request and deploy model artifact
166
+
167
+ :param upload_id: Multi-part upload id
168
+ :param parts: List of all parts
169
+ :param deployment_params: Deployment parameters
170
+ :return: Async job
171
+ """
172
+ method = self._get_method(update)
173
+ payload: dict[str, Any] = {
174
+ 'upload_id': upload_id,
175
+ 'parts': parts,
176
+ }
177
+
178
+ if deployment_params:
179
+ payload['deployment_params'] = deployment_params.dict(exclude_unset=True)
180
+
181
+ response = method(
182
+ url=f'/v3/models/{self.id}/deploy-artifact-multi-part-complete',
183
+ data=payload,
184
+ )
185
+
186
+ logger.info(
187
+ 'Model[%s] - Multi-part model upload completed',
188
+ self.id,
189
+ )
190
+ job_compact = JobCompactResp(**response.json()['data']['job'])
191
+ return Job.get(id_=job_compact.id)
192
+
193
+ def _artifact_deploy_multipart(
194
+ self,
195
+ file_path: Path,
196
+ deployment_params: DeploymentParams | None = None,
197
+ update: bool = False,
198
+ ) -> Job:
199
+ """
200
+ Upload and deploy model artifact
201
+
202
+ :param file_path: Path to model artifact tar file
203
+ :param deployment_params: Model deployment parameters
204
+ :param update: Flag for add or update artifacts
205
+ :return: Async job uuid
206
+ """
207
+ # 1. Initialize multi-part upload request
208
+ upload_id = self._initialize_multi_part_upload(update)
209
+ part_number = 1
210
+ parts = []
211
+ file_size = os.path.getsize(file_path)
212
+ total_chunks = math.ceil(file_size / MULTI_PART_CHUNK_SIZE)
213
+
214
+ # 2. Chunk and upload
215
+ with open(file_path, 'rb') as f:
216
+ while True:
217
+ data = f.read(MULTI_PART_CHUNK_SIZE)
218
+ if not data:
219
+ break
220
+
221
+ logger.info(
222
+ 'Model[%s] - Uploading multi-part chunk - %d/%d',
223
+ self.id,
224
+ part_number,
225
+ total_chunks,
226
+ )
227
+
228
+ part = self._upload_multi_part_chunk(
229
+ data=data,
230
+ upload_id=upload_id,
231
+ part_number=part_number,
232
+ update=update,
233
+ )
234
+ parts.append(part)
235
+ logger.info(
236
+ 'Model[%s] - Uploaded multi-part chunk - %d/%d',
237
+ self.id,
238
+ part_number,
239
+ total_chunks,
240
+ )
241
+ part_number += 1
242
+
243
+ # 3: Complete the upload
244
+ return self._complete_multi_part_upload(
245
+ upload_id=upload_id,
246
+ parts=parts,
247
+ update=update,
248
+ deployment_params=deployment_params,
249
+ )
250
+
251
+ def _artifact_deploy(
252
+ self,
253
+ file_path: Path,
254
+ deployment_params: DeploymentParams | None = None,
255
+ update: bool = False,
256
+ ) -> Job:
257
+ """Artifact deploy base method."""
258
+ method = self._get_method(update)
259
+ params = {}
260
+ if deployment_params:
261
+ params['deployment_params'] = json.dumps(
262
+ deployment_params.dict(exclude_unset=True)
263
+ )
264
+
265
+ with open(file_path, 'rb') as f:
266
+ data = f.read()
267
+ response = method(
268
+ url=f'/v3/models/{self.id}/deploy-artifact',
269
+ params=params,
270
+ data=data,
271
+ headers=CONTENT_TYPE_OCTET_STREAM_HEADER,
272
+ )
273
+
274
+ job_compact = JobCompactResp(**response.json()['data']['job'])
275
+ return Job.get(id_=job_compact.id)
276
+
277
+ @handle_api_error
278
+ def add_artifact(
279
+ self,
280
+ model_dir: str | Path,
281
+ deployment_params: DeploymentParams | None = None,
282
+ ) -> Job:
283
+ """
284
+ Upload and deploy model artifact.
285
+
286
+ :param model_dir: Path to model artifact tar file
287
+ :param deployment_params: Model deployment parameters
288
+ :return: Async job.
289
+ """
290
+ self._check_id_attributes()
291
+ job = self._deploy_model_artifact(
292
+ model_dir=model_dir, deployment_params=deployment_params
293
+ )
294
+ return job
295
+
296
+ @handle_api_error
297
+ def update_artifact(
298
+ self,
299
+ model_dir: str | Path,
300
+ deployment_params: DeploymentParams | None = None,
301
+ ) -> Job:
302
+ """
303
+ Update existing model artifact.
304
+
305
+ :param model_dir: Path to model artifact tar file
306
+ :param deployment_params: Model deployment parameters
307
+ :return: Async job.
308
+ """
309
+ self._check_id_attributes()
310
+ job = self._deploy_model_artifact(
311
+ model_dir=model_dir, deployment_params=deployment_params, update=True
312
+ )
313
+ return job
314
+
315
+ @handle_api_error
316
+ def download_artifact(
317
+ self,
318
+ output_dir: str | Path,
319
+ ) -> None:
320
+ """
321
+ Download existing model artifact.
322
+
323
+ :param output_dir: Path to download model artifact tar file
324
+ """
325
+ self._check_id_attributes()
326
+ output_dir = Path(output_dir)
327
+ if output_dir.exists():
328
+ raise ValueError(f'Output dir already exists {output_dir}')
329
+
330
+ with tempfile.TemporaryDirectory() as tmp_dir:
331
+ # Download tar file
332
+ tar_file_path = os.path.join(tmp_dir, 'artifact.tar')
333
+
334
+ with self._client().get(
335
+ url=f'/v3/models/{self.id}/download-artifact'
336
+ ) as resp:
337
+ resp.raise_for_status()
338
+ with open(tar_file_path, mode='wb') as f:
339
+ for chunk in resp.iter_content(chunk_size=8192):
340
+ if chunk:
341
+ f.write(chunk)
342
+ os.makedirs(output_dir, exist_ok=True)
343
+ shutil.unpack_archive(tar_file_path, extract_dir=output_dir, format='tar')
344
+
345
+ def _check_id_attributes(self) -> None:
346
+ if not self.id:
347
+ raise AttributeError(
348
+ 'This method is available only for model object generated from '
349
+ 'API response.'
350
+ )