fiddler-client 2.4.0.dev2__py3-none-any.whl → 2.5.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.
- fiddler/__init__.py +2 -0
- fiddler/_version.py +1 -1
- fiddler/api/alert_mixin.py +51 -1
- fiddler/api/webhooks_mixin.py +6 -8
- fiddler/core_objects.py +67 -30
- fiddler/schema/alert.py +8 -1
- fiddler/schemas/custom_features.py +13 -0
- fiddler3/__init__.py +1 -36
- fiddler3/connection.py +68 -1
- fiddler3/constants/alert_rule.py +30 -0
- fiddler3/constants/events.py +7 -0
- fiddler3/entities/__init__.py +3 -0
- fiddler3/entities/alert_record.py +106 -0
- fiddler3/entities/alert_rule.py +246 -0
- fiddler3/entities/base.py +2 -16
- fiddler3/entities/events.py +120 -0
- fiddler3/entities/files.py +170 -0
- fiddler3/entities/helpers.py +6 -6
- fiddler3/entities/model.py +67 -18
- fiddler3/entities/model_artifact.py +48 -220
- fiddler3/entities/project.py +1 -1
- fiddler3/entities/webhook.py +128 -0
- fiddler3/entities/xai.py +267 -0
- fiddler3/libs/json_encoder.py +1 -1
- fiddler3/schemas/alert_record.py +27 -0
- fiddler3/schemas/alert_rule.py +32 -0
- fiddler3/schemas/custom_features.py +34 -0
- fiddler3/schemas/events.py +15 -0
- fiddler3/schemas/files.py +23 -0
- fiddler3/schemas/model.py +1 -6
- fiddler3/schemas/webhook.py +26 -0
- fiddler3/tests/apis/test_alert_record.py +99 -0
- fiddler3/tests/apis/test_alert_rule.py +323 -0
- fiddler3/tests/apis/test_events.py +128 -0
- fiddler3/tests/apis/test_files.py +164 -0
- fiddler3/tests/apis/test_model.py +55 -8
- fiddler3/tests/apis/test_model_artifact.py +21 -0
- fiddler3/tests/apis/test_project.py +2 -2
- fiddler3/tests/apis/test_webhook.py +280 -0
- fiddler3/tests/apis/test_xai.py +233 -1
- fiddler3/tests/conftest.py +2 -2
- fiddler3/tests/constants.py +5 -0
- fiddler3/tests/test_connection.py +24 -1
- fiddler3/tests/test_json_encoder.py +2 -2
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/METADATA +1 -1
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/RECORD +50 -34
- tests/fiddler/test_alert.py +95 -5
- fiddler3/schemas/model_artifact.py +0 -6
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/LICENSE.txt +0 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/WHEEL +0 -0
- {fiddler_client-2.4.0.dev2.dist-info → fiddler_client-2.5.0.dev1.dist-info}/top_level.txt +0 -0
fiddler3/entities/model.py
CHANGED
|
@@ -4,6 +4,7 @@ import builtins
|
|
|
4
4
|
import typing
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from functools import cached_property
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Iterator
|
|
9
10
|
from uuid import UUID
|
|
@@ -13,17 +14,20 @@ import pandas as pd
|
|
|
13
14
|
from fiddler3.constants.model import ModelInputType, ModelTask
|
|
14
15
|
from fiddler3.decorators import handle_api_error
|
|
15
16
|
from fiddler3.entities.base import BaseEntity
|
|
17
|
+
from fiddler3.entities.events import EventMixin
|
|
16
18
|
from fiddler3.entities.helpers import raise_not_found
|
|
17
19
|
from fiddler3.entities.job import Job
|
|
18
|
-
from fiddler3.entities.model_artifact import
|
|
20
|
+
from fiddler3.entities.model_artifact import ModelArtifact
|
|
21
|
+
from fiddler3.entities.model_deployment import ModelDeployment
|
|
19
22
|
from fiddler3.entities.model_surrogate import SurrogateMixin
|
|
20
23
|
from fiddler3.entities.project import ProjectCompactMixin
|
|
21
24
|
from fiddler3.entities.user import CreatedByMixin, UpdatedByMixin
|
|
22
25
|
from fiddler3.entities.xai import XaiMixin
|
|
26
|
+
from fiddler3.schemas.deployment_params import DeploymentParams
|
|
23
27
|
from fiddler3.schemas.filter_query import OperatorType, QueryCondition, QueryRule
|
|
24
28
|
from fiddler3.schemas.job import JobCompactResp
|
|
25
29
|
from fiddler3.schemas.model import ModelResp
|
|
26
|
-
from fiddler3.schemas.model_schema import
|
|
30
|
+
from fiddler3.schemas.model_schema import ModelSchema
|
|
27
31
|
from fiddler3.schemas.model_spec import ModelSpec
|
|
28
32
|
from fiddler3.schemas.model_task_params import ModelTaskParams
|
|
29
33
|
from fiddler3.schemas.xai_params import XaiParams
|
|
@@ -35,12 +39,12 @@ if typing.TYPE_CHECKING:
|
|
|
35
39
|
|
|
36
40
|
class Model(
|
|
37
41
|
BaseEntity,
|
|
38
|
-
ArtifactMixin,
|
|
39
42
|
CreatedByMixin,
|
|
40
43
|
ProjectCompactMixin,
|
|
41
44
|
UpdatedByMixin,
|
|
42
45
|
SurrogateMixin,
|
|
43
46
|
XaiMixin,
|
|
47
|
+
EventMixin,
|
|
44
48
|
): # pylint: disable=too-many-ancestors
|
|
45
49
|
def __init__( # pylint: disable=too-many-arguments
|
|
46
50
|
self,
|
|
@@ -74,11 +78,6 @@ class Model(
|
|
|
74
78
|
self.id: UUID | None = None
|
|
75
79
|
self.artifact_status: str | None = None
|
|
76
80
|
self.artifact_files: list[dict] | None = None
|
|
77
|
-
self.input_cols: list[Column] | None = None
|
|
78
|
-
self.output_cols: list[Column] | None = None
|
|
79
|
-
self.target_cols: list[Column] | None = None
|
|
80
|
-
self.metadata_cols: list[Column] | None = None
|
|
81
|
-
self.decision_cols: list[Column] | None = None
|
|
82
81
|
self.is_binary_ranking_model: bool | None = None
|
|
83
82
|
self.created_at: datetime | None = None
|
|
84
83
|
self.updated_at: datetime | None = None
|
|
@@ -122,11 +121,6 @@ class Model(
|
|
|
122
121
|
'updated_at',
|
|
123
122
|
'artifact_status',
|
|
124
123
|
'artifact_files',
|
|
125
|
-
'input_cols',
|
|
126
|
-
'output_cols',
|
|
127
|
-
'target_cols',
|
|
128
|
-
'metadata_cols',
|
|
129
|
-
'decision_cols',
|
|
130
124
|
'is_binary_ranking_model',
|
|
131
125
|
]
|
|
132
126
|
for field in fields:
|
|
@@ -160,11 +154,6 @@ class Model(
|
|
|
160
154
|
'updated_at',
|
|
161
155
|
'artifact_status',
|
|
162
156
|
'artifact_files',
|
|
163
|
-
'input_cols',
|
|
164
|
-
'output_cols',
|
|
165
|
-
'target_cols',
|
|
166
|
-
'metadata_cols',
|
|
167
|
-
'decision_cols',
|
|
168
157
|
'is_binary_ranking_model',
|
|
169
158
|
]
|
|
170
159
|
for field in fields:
|
|
@@ -172,6 +161,12 @@ class Model(
|
|
|
172
161
|
|
|
173
162
|
self._resp = resp_obj
|
|
174
163
|
|
|
164
|
+
@cached_property
|
|
165
|
+
def _artifact(self) -> ModelArtifact:
|
|
166
|
+
"""Model artifact instance"""
|
|
167
|
+
assert self.id is not None
|
|
168
|
+
return ModelArtifact(model_id=self.id)
|
|
169
|
+
|
|
175
170
|
@classmethod
|
|
176
171
|
@handle_api_error
|
|
177
172
|
def get(cls, id_: UUID | str) -> Model:
|
|
@@ -268,6 +263,16 @@ class Model(
|
|
|
268
263
|
|
|
269
264
|
yield from Dataset.list(model_id=self.id)
|
|
270
265
|
|
|
266
|
+
@property
|
|
267
|
+
@handle_api_error
|
|
268
|
+
def model_deployment(self) -> ModelDeployment:
|
|
269
|
+
"""Fetch all the deployments for this model"""
|
|
270
|
+
url = f'{self._get_url(id_=self.id)}/deployment'
|
|
271
|
+
response = self._client().get(url=url)
|
|
272
|
+
return ModelDeployment._from_dict( # pylint: disable=protected-access
|
|
273
|
+
data=response.json()['data']
|
|
274
|
+
)
|
|
275
|
+
|
|
271
276
|
@classmethod
|
|
272
277
|
@handle_api_error
|
|
273
278
|
def generate_schema(
|
|
@@ -304,6 +309,50 @@ class Model(
|
|
|
304
309
|
job_compact = JobCompactResp(**response.json()['data']['job'])
|
|
305
310
|
return Job.get(id_=job_compact.id)
|
|
306
311
|
|
|
312
|
+
def add_artifact(
|
|
313
|
+
self,
|
|
314
|
+
model_dir: str | Path,
|
|
315
|
+
deployment_params: DeploymentParams | None = None,
|
|
316
|
+
) -> Job:
|
|
317
|
+
"""
|
|
318
|
+
Upload and deploy model artifact.
|
|
319
|
+
|
|
320
|
+
:param model_dir: Path to model artifact tar file
|
|
321
|
+
:param deployment_params: Model deployment parameters
|
|
322
|
+
:return: Async job instance
|
|
323
|
+
"""
|
|
324
|
+
return self._artifact.add(
|
|
325
|
+
model_dir=model_dir, deployment_params=deployment_params
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def update_artifact(
|
|
329
|
+
self,
|
|
330
|
+
model_dir: str | Path,
|
|
331
|
+
deployment_params: DeploymentParams | None = None,
|
|
332
|
+
) -> Job:
|
|
333
|
+
"""
|
|
334
|
+
Update existing model artifact.
|
|
335
|
+
|
|
336
|
+
:param model_dir: Path to model artifact tar file
|
|
337
|
+
:param deployment_params: Model deployment parameters
|
|
338
|
+
:return: Async job instance
|
|
339
|
+
"""
|
|
340
|
+
return self._artifact.update(
|
|
341
|
+
model_dir=model_dir, deployment_params=deployment_params
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def download_artifact(
|
|
345
|
+
self,
|
|
346
|
+
output_dir: str | Path,
|
|
347
|
+
) -> None:
|
|
348
|
+
"""
|
|
349
|
+
Download existing model artifact.
|
|
350
|
+
|
|
351
|
+
:param output_dir: Path to download model artifact tar file
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
self._artifact.download(output_dir=output_dir)
|
|
355
|
+
|
|
307
356
|
|
|
308
357
|
@dataclass
|
|
309
358
|
class ModelCompact:
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
|
-
import math
|
|
5
3
|
import os
|
|
6
4
|
import shutil
|
|
7
5
|
import tempfile
|
|
8
6
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
7
|
+
from typing import Callable
|
|
10
8
|
from uuid import UUID
|
|
11
9
|
|
|
12
|
-
from fiddler3.
|
|
13
|
-
CONTENT_TYPE_OCTET_STREAM_HEADER,
|
|
14
|
-
MULTI_PART_CHUNK_SIZE,
|
|
15
|
-
)
|
|
10
|
+
from fiddler3.connection import ConnectionMixin
|
|
16
11
|
from fiddler3.decorators import handle_api_error
|
|
12
|
+
from fiddler3.entities.files import File
|
|
17
13
|
from fiddler3.entities.job import Job
|
|
18
|
-
from fiddler3.schemas.deployment_params import
|
|
14
|
+
from fiddler3.schemas.deployment_params import DeploymentParams
|
|
19
15
|
from fiddler3.schemas.job import JobCompactResp
|
|
20
|
-
from fiddler3.schemas.model_artifact import ModelArtifactDeployMultiPartUploadResp
|
|
21
16
|
from fiddler3.utils.logger import get_logger
|
|
22
17
|
from fiddler3.utils.validations import validate_artifact_dir
|
|
23
18
|
|
|
24
19
|
logger = get_logger(__name__)
|
|
25
20
|
|
|
26
21
|
|
|
27
|
-
class
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
class ModelArtifact(ConnectionMixin):
|
|
23
|
+
def __init__(self, model_id: UUID) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Model artifact methods
|
|
26
|
+
|
|
27
|
+
:param model_id: Model identifier
|
|
28
|
+
"""
|
|
29
|
+
self.model_id = model_id
|
|
30
30
|
|
|
31
31
|
def _get_method(self, update: bool = False) -> Callable:
|
|
32
|
-
"""Get HTTP method"""
|
|
33
|
-
|
|
32
|
+
"""Get HTTP method based on operation"""
|
|
33
|
+
if update:
|
|
34
|
+
return self._client().put
|
|
35
|
+
return self._client().post
|
|
34
36
|
|
|
35
37
|
def _deploy_model_artifact(
|
|
36
38
|
self,
|
|
@@ -49,20 +51,11 @@ class ArtifactMixin:
|
|
|
49
51
|
model_dir = Path(model_dir)
|
|
50
52
|
validate_artifact_dir(model_dir)
|
|
51
53
|
|
|
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
54
|
with tempfile.TemporaryDirectory() as tmp:
|
|
62
55
|
# Archive model artifact directory
|
|
63
56
|
logger.info(
|
|
64
57
|
'Model[%s] - Tarring model artifact directory - %s',
|
|
65
|
-
self.
|
|
58
|
+
self.model_id,
|
|
66
59
|
model_dir,
|
|
67
60
|
)
|
|
68
61
|
file_path = shutil.make_archive(
|
|
@@ -74,208 +67,54 @@ class ArtifactMixin:
|
|
|
74
67
|
|
|
75
68
|
logger.info(
|
|
76
69
|
'Model[%s] - Model artifact tar file created at %s',
|
|
77
|
-
self.
|
|
70
|
+
self.model_id,
|
|
78
71
|
file_path,
|
|
79
72
|
)
|
|
80
73
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
deployment_params=deployment_params,
|
|
92
|
-
update=update,
|
|
93
|
-
)
|
|
74
|
+
# Upload file
|
|
75
|
+
file = File(path=file_path).upload()
|
|
76
|
+
|
|
77
|
+
# Deploy artifact
|
|
78
|
+
assert file.id is not None
|
|
79
|
+
job = self._artifact_deploy(
|
|
80
|
+
file_id=file.id,
|
|
81
|
+
deployment_params=deployment_params,
|
|
82
|
+
update=update,
|
|
83
|
+
)
|
|
94
84
|
|
|
95
85
|
logger.info(
|
|
96
86
|
'Model[%s] - Submitted job (%s) for deploying model artifact',
|
|
97
|
-
self.
|
|
87
|
+
self.model_id,
|
|
98
88
|
job.id,
|
|
99
89
|
)
|
|
100
90
|
|
|
101
91
|
return job
|
|
102
92
|
|
|
103
|
-
def
|
|
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(
|
|
93
|
+
def _artifact_deploy(
|
|
158
94
|
self,
|
|
159
|
-
|
|
160
|
-
parts: list[dict],
|
|
161
|
-
update: bool,
|
|
95
|
+
file_id: UUID,
|
|
162
96
|
deployment_params: DeploymentParams | None = None,
|
|
97
|
+
update: bool = False,
|
|
163
98
|
) -> 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
|
-
"""
|
|
99
|
+
"""Artifact deploy base method."""
|
|
172
100
|
method = self._get_method(update)
|
|
173
|
-
payload
|
|
174
|
-
'
|
|
175
|
-
'
|
|
101
|
+
payload = {
|
|
102
|
+
'file_id': file_id,
|
|
103
|
+
'deployment_params': deployment_params.dict(exclude_unset=True)
|
|
104
|
+
if deployment_params
|
|
105
|
+
else {},
|
|
176
106
|
}
|
|
177
107
|
|
|
178
|
-
if deployment_params:
|
|
179
|
-
payload['deployment_params'] = deployment_params.dict(exclude_unset=True)
|
|
180
|
-
|
|
181
108
|
response = method(
|
|
182
|
-
url=f'/v3/models/{self.
|
|
109
|
+
url=f'/v3/models/{self.model_id}/deploy-artifact',
|
|
183
110
|
data=payload,
|
|
184
111
|
)
|
|
185
112
|
|
|
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
113
|
job_compact = JobCompactResp(**response.json()['data']['job'])
|
|
275
114
|
return Job.get(id_=job_compact.id)
|
|
276
115
|
|
|
277
116
|
@handle_api_error
|
|
278
|
-
def
|
|
117
|
+
def add(
|
|
279
118
|
self,
|
|
280
119
|
model_dir: str | Path,
|
|
281
120
|
deployment_params: DeploymentParams | None = None,
|
|
@@ -285,16 +124,14 @@ class ArtifactMixin:
|
|
|
285
124
|
|
|
286
125
|
:param model_dir: Path to model artifact tar file
|
|
287
126
|
:param deployment_params: Model deployment parameters
|
|
288
|
-
:return: Async job
|
|
127
|
+
:return: Async job instance
|
|
289
128
|
"""
|
|
290
|
-
self.
|
|
291
|
-
job = self._deploy_model_artifact(
|
|
129
|
+
return self._deploy_model_artifact(
|
|
292
130
|
model_dir=model_dir, deployment_params=deployment_params
|
|
293
131
|
)
|
|
294
|
-
return job
|
|
295
132
|
|
|
296
133
|
@handle_api_error
|
|
297
|
-
def
|
|
134
|
+
def update(
|
|
298
135
|
self,
|
|
299
136
|
model_dir: str | Path,
|
|
300
137
|
deployment_params: DeploymentParams | None = None,
|
|
@@ -304,16 +141,14 @@ class ArtifactMixin:
|
|
|
304
141
|
|
|
305
142
|
:param model_dir: Path to model artifact tar file
|
|
306
143
|
:param deployment_params: Model deployment parameters
|
|
307
|
-
:return: Async job
|
|
144
|
+
:return: Async job instance
|
|
308
145
|
"""
|
|
309
|
-
self.
|
|
310
|
-
job = self._deploy_model_artifact(
|
|
146
|
+
return self._deploy_model_artifact(
|
|
311
147
|
model_dir=model_dir, deployment_params=deployment_params, update=True
|
|
312
148
|
)
|
|
313
|
-
return job
|
|
314
149
|
|
|
315
150
|
@handle_api_error
|
|
316
|
-
def
|
|
151
|
+
def download(
|
|
317
152
|
self,
|
|
318
153
|
output_dir: str | Path,
|
|
319
154
|
) -> None:
|
|
@@ -322,7 +157,7 @@ class ArtifactMixin:
|
|
|
322
157
|
|
|
323
158
|
:param output_dir: Path to download model artifact tar file
|
|
324
159
|
"""
|
|
325
|
-
|
|
160
|
+
|
|
326
161
|
output_dir = Path(output_dir)
|
|
327
162
|
if output_dir.exists():
|
|
328
163
|
raise ValueError(f'Output dir already exists {output_dir}')
|
|
@@ -332,7 +167,7 @@ class ArtifactMixin:
|
|
|
332
167
|
tar_file_path = os.path.join(tmp_dir, 'artifact.tar')
|
|
333
168
|
|
|
334
169
|
with self._client().get(
|
|
335
|
-
url=f'/v3/models/{self.
|
|
170
|
+
url=f'/v3/models/{self.model_id}/download-artifact'
|
|
336
171
|
) as resp:
|
|
337
172
|
resp.raise_for_status()
|
|
338
173
|
with open(tar_file_path, mode='wb') as f:
|
|
@@ -341,10 +176,3 @@ class ArtifactMixin:
|
|
|
341
176
|
f.write(chunk)
|
|
342
177
|
os.makedirs(output_dir, exist_ok=True)
|
|
343
178
|
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
|
-
)
|
fiddler3/entities/project.py
CHANGED
|
@@ -115,7 +115,7 @@ class Project(BaseEntity):
|
|
|
115
115
|
def delete(self) -> None:
|
|
116
116
|
"""Delete project"""
|
|
117
117
|
# @TODO - Switch this to v3 once we have the endpoint
|
|
118
|
-
self._client().delete(url=f'/v2/projects/{self.
|
|
118
|
+
self._client().delete(url=f'/v2/projects/{self.organization_name}:{self.name}')
|
|
119
119
|
|
|
120
120
|
@property
|
|
121
121
|
def models(self) -> Iterator[ModelCompact]:
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Iterator
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fiddler3.decorators import handle_api_error
|
|
8
|
+
from fiddler3.entities.base import BaseEntity
|
|
9
|
+
from fiddler3.schemas.webhook import WebhookProvider, WebhookResp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Webhook(BaseEntity):
|
|
13
|
+
def __init__(self, name: str, url: str, provider: WebhookProvider | str) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Construct a webhook instance
|
|
16
|
+
|
|
17
|
+
:param name: Slug like name
|
|
18
|
+
"""
|
|
19
|
+
self.name = name
|
|
20
|
+
self.url = url
|
|
21
|
+
# provider is 'SLACK' or 'OTHER' as of Aug 2023.
|
|
22
|
+
self.provider = provider
|
|
23
|
+
|
|
24
|
+
self.id: UUID | None = None
|
|
25
|
+
self.created_at: datetime | None = None
|
|
26
|
+
self.updated_at: datetime | None = None
|
|
27
|
+
|
|
28
|
+
# Deserialized response object
|
|
29
|
+
self._resp: WebhookResp | None = None
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _get_url(id_: UUID | str | None = None) -> str:
|
|
33
|
+
"""Get webhook resource/item url"""
|
|
34
|
+
url = '/v2/webhooks'
|
|
35
|
+
return url if not id_ else f'{url}/{id_}'
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _from_dict(cls, data: dict) -> Webhook:
|
|
39
|
+
"""Build entity object from the given dictionary"""
|
|
40
|
+
|
|
41
|
+
# Deserialize the response
|
|
42
|
+
resp_obj = WebhookResp(**data)
|
|
43
|
+
|
|
44
|
+
# Initialize
|
|
45
|
+
instance = cls(name=resp_obj.name, url=resp_obj.url, provider=resp_obj.provider)
|
|
46
|
+
# Add remaining fields
|
|
47
|
+
fields = ['id', 'created_at', 'updated_at']
|
|
48
|
+
for field in fields:
|
|
49
|
+
setattr(instance, field, getattr(resp_obj, field, None))
|
|
50
|
+
|
|
51
|
+
instance._resp = resp_obj
|
|
52
|
+
return instance
|
|
53
|
+
|
|
54
|
+
def _refresh(self, data: dict) -> None:
|
|
55
|
+
"""Refresh the fields of this instance from the given response dictionary"""
|
|
56
|
+
# Deserialize the response
|
|
57
|
+
resp_obj = WebhookResp(**data)
|
|
58
|
+
|
|
59
|
+
fields = [
|
|
60
|
+
'id',
|
|
61
|
+
'name',
|
|
62
|
+
'url',
|
|
63
|
+
'provider',
|
|
64
|
+
'created_at',
|
|
65
|
+
'updated_at',
|
|
66
|
+
]
|
|
67
|
+
for field in fields:
|
|
68
|
+
setattr(self, field, getattr(resp_obj, field, None))
|
|
69
|
+
|
|
70
|
+
self._resp = resp_obj
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
@handle_api_error
|
|
74
|
+
def get(cls, id_: UUID | str) -> Webhook:
|
|
75
|
+
"""
|
|
76
|
+
Get the webhook instance using webhook id
|
|
77
|
+
|
|
78
|
+
:params uuid: UUID belongs to the Webhook
|
|
79
|
+
:returns: `Webhook` object
|
|
80
|
+
"""
|
|
81
|
+
response = cls._client().get(url=cls._get_url(id_))
|
|
82
|
+
return cls._from_response(response=response)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
@handle_api_error
|
|
86
|
+
def list(cls) -> Iterator[Webhook]:
|
|
87
|
+
"""Get a list of all webhooks in the organization"""
|
|
88
|
+
for webhook in cls._paginate(url=cls._get_url()):
|
|
89
|
+
yield cls._from_dict(data=webhook)
|
|
90
|
+
|
|
91
|
+
@handle_api_error
|
|
92
|
+
def create(self) -> Webhook:
|
|
93
|
+
"""
|
|
94
|
+
Create a new webhook
|
|
95
|
+
|
|
96
|
+
:params name: name of webhook
|
|
97
|
+
:params url: webhook url
|
|
98
|
+
:params provider: Either 'SLACK' or 'OTHER'
|
|
99
|
+
|
|
100
|
+
:returns: Created `Webhook` object.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
request_body = {
|
|
104
|
+
'name': self.name,
|
|
105
|
+
'url': self.url,
|
|
106
|
+
'provider': self.provider,
|
|
107
|
+
}
|
|
108
|
+
response = self._client().post(url=self._get_url(), data=request_body)
|
|
109
|
+
self._refresh_from_response(response=response)
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
@handle_api_error
|
|
113
|
+
def update(self) -> None:
|
|
114
|
+
"""Update an existing webhook."""
|
|
115
|
+
body: dict[str, Any] = {
|
|
116
|
+
'name': self.name,
|
|
117
|
+
'url': self.url,
|
|
118
|
+
'provider': self.provider,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
response = self._client().patch(url=self._get_url(id_=self.id), data=body)
|
|
122
|
+
self._refresh_from_response(response=response)
|
|
123
|
+
|
|
124
|
+
@handle_api_error
|
|
125
|
+
def delete(self) -> None:
|
|
126
|
+
"""Delete an existing webhook."""
|
|
127
|
+
|
|
128
|
+
self._client().delete(url=self._get_url(id_=self.id))
|