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
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from fiddler3.decorators import handle_api_error
8
+ from fiddler3.entities.base import BaseEntity
9
+ from fiddler3.entities.project import ProjectCompactMixin
10
+ from fiddler3.schemas.model_deployment import ModelDeploymentResponse
11
+ from fiddler3.utils.logger import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class ModelDeployment(
17
+ BaseEntity, ProjectCompactMixin
18
+ ): # pylint: disable=too-many-instance-attributes
19
+ def __init__( # pylint: disable=too-many-arguments
20
+ self,
21
+ model_id: UUID,
22
+ ) -> None:
23
+ """Construct a model deployment instance."""
24
+ self.id: UUID | None = None # pylint: disable=invalid-name
25
+ self.model_id = model_id
26
+ self.artifact_type: str | None = None
27
+ self.deployment_type: str | None = None
28
+ self.active: bool | None = None
29
+ self.image_uri: str | None = None
30
+ self.replicas: int | None = None
31
+ self.cpu: int | None = None
32
+ self.memory: int | None = None
33
+ self.created_at: datetime | None = None
34
+ self.updated_at: datetime | None = None
35
+
36
+ # Deserialized response object
37
+ self._resp: ModelDeploymentResponse | None = None
38
+
39
+ @staticmethod
40
+ def _get_url(model_id: UUID | str | None = None) -> str:
41
+ """Get model deployment url."""
42
+ return f'v3/models/{model_id}/deployment'
43
+
44
+ @classmethod
45
+ def _from_dict(cls, data: dict) -> ModelDeployment:
46
+ """Build entity object from the given dictionary."""
47
+
48
+ # Deserialize the response
49
+ resp_obj = ModelDeploymentResponse(**data)
50
+ # Initialize
51
+ instance = cls(
52
+ model_id=resp_obj.model.id,
53
+ )
54
+
55
+ # Add remaining fields
56
+ fields = [
57
+ 'id',
58
+ 'artifact_type',
59
+ 'deployment_type',
60
+ 'active',
61
+ 'image_uri',
62
+ 'replicas',
63
+ 'cpu',
64
+ 'memory',
65
+ 'created_at',
66
+ 'updated_at',
67
+ ]
68
+ for field in fields:
69
+ setattr(instance, field, getattr(resp_obj, field, None))
70
+
71
+ instance._resp = resp_obj
72
+ return instance
73
+
74
+ def _refresh(self, data: dict) -> None:
75
+ """Refresh the fields of this instance from the given response dictionary"""
76
+ # Deserialize the response
77
+ resp_obj = ModelDeploymentResponse(**data)
78
+
79
+ fields = [
80
+ 'id',
81
+ 'artifact_type',
82
+ 'deployment_type',
83
+ 'active',
84
+ 'image_uri',
85
+ 'replicas',
86
+ 'cpu',
87
+ 'memory',
88
+ 'created_at',
89
+ 'updated_at',
90
+ ]
91
+ for field in fields:
92
+ setattr(self, field, getattr(resp_obj, field, None))
93
+
94
+ self._resp = resp_obj
95
+
96
+ @handle_api_error
97
+ def update(self) -> None:
98
+ """Update an existing model deployment."""
99
+ body: dict[str, Any] = {
100
+ 'artifact_type': self.artifact_type,
101
+ 'deployment_type': self.deployment_type,
102
+ 'active': self.active,
103
+ 'image_uri': self.image_uri,
104
+ 'replicas': self.replicas,
105
+ 'cpu': self.cpu,
106
+ 'memory': self.memory,
107
+ }
108
+
109
+ response = self._client().patch(
110
+ url=self._get_url(model_id=self.model_id), data=body
111
+ )
112
+ self._refresh_from_response(response=response)
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+ from uuid import UUID
5
+
6
+ from fiddler3.decorators import handle_api_error
7
+ from fiddler3.entities.job import Job
8
+ from fiddler3.schemas.deployment_params import ArtifactType, DeploymentParams
9
+ from fiddler3.schemas.job import JobCompactResp
10
+ from fiddler3.utils.logger import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class SurrogateMixin:
16
+ id: UUID | None
17
+ _client: Callable
18
+
19
+ def _check_id_attributes(self) -> None:
20
+ if not self.id:
21
+ raise AttributeError(
22
+ 'This method is available only for model object generated from '
23
+ 'API response.'
24
+ )
25
+
26
+ def _deploy_surrogate_model(
27
+ self,
28
+ deployment_params: DeploymentParams | None = None,
29
+ update: bool = False,
30
+ ) -> Job:
31
+ """
32
+ Add surrogate model to an existing model
33
+
34
+ :param deployment_params: Model deployment parameters
35
+ :param update: Set True for re-generating surrogate model, otherwise False
36
+ :return: Async job uuid
37
+ """
38
+ payload: dict[str, Any] = {}
39
+
40
+ if deployment_params:
41
+ deployment_params.artifact_type = ArtifactType.SURROGATE
42
+ payload.update(
43
+ {'deployment_params': deployment_params.dict(exclude_unset=True)}
44
+ )
45
+
46
+ url = f'/v3/models/{self.id}/deploy-surrogate'
47
+ method: Callable = self._client().put if update else self._client().post
48
+ response = method(url=url, data=payload)
49
+
50
+ job_compact = JobCompactResp(**response.json()['data']['job'])
51
+
52
+ logger.info(
53
+ 'Model[%s] - Submitted job (%s) for deploying a surrogate model',
54
+ self.id,
55
+ job_compact.id,
56
+ )
57
+
58
+ return Job.get(id_=job_compact.id)
59
+
60
+ @handle_api_error
61
+ def add_surrogate(
62
+ self,
63
+ deployment_params: DeploymentParams | None = None,
64
+ ) -> Job:
65
+ """
66
+ Add a new surrogate.
67
+
68
+ :param deployment_params: Model deployment parameters
69
+ :return: Async job.
70
+ """
71
+ self._check_id_attributes()
72
+ job = self._deploy_surrogate_model(
73
+ deployment_params=deployment_params, update=False
74
+ )
75
+ return job
76
+
77
+ @handle_api_error
78
+ def update_surrogate(
79
+ self,
80
+ deployment_params: DeploymentParams | None = None,
81
+ ) -> Job:
82
+ """
83
+ Update an existing surrogate.
84
+
85
+ :param deployment_params: Model deployment parameters
86
+ :return: Async job.
87
+ """
88
+ self._check_id_attributes()
89
+ job = self._deploy_surrogate_model(
90
+ deployment_params=deployment_params, update=True
91
+ )
92
+ return job
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from uuid import UUID
5
+
6
+ from fiddler3.entities.base import BaseEntity
7
+ from fiddler3.schemas.organization import OrganizationResp
8
+
9
+
10
+ class Organization(BaseEntity):
11
+ def __init__(self) -> None:
12
+ """Construct a organization instance"""
13
+
14
+ self.name: str | None = None
15
+ self.id: UUID | None = None
16
+ self.created_at: datetime | None = None
17
+ self.updated_at: datetime | None = None
18
+
19
+ # Deserialized response object
20
+ self._resp: OrganizationResp | None = None
21
+
22
+ @classmethod
23
+ def _from_dict(cls, data: dict) -> Organization:
24
+ """Build entity object from the given dictionary"""
25
+
26
+ # Deserialize the response
27
+ resp_obj = OrganizationResp(**data)
28
+
29
+ # Initialize
30
+ instance = cls()
31
+
32
+ # Add remaining fields
33
+ fields = ['id', 'name', 'created_at', 'updated_at']
34
+ for field in fields:
35
+ setattr(instance, field, getattr(resp_obj, field, None))
36
+
37
+ instance._resp = resp_obj
38
+ return instance
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import typing
4
+ from dataclasses import dataclass
3
5
  from datetime import datetime
4
6
  from typing import Iterator
5
7
  from uuid import UUID
@@ -7,13 +9,18 @@ from uuid import UUID
7
9
  from fiddler3.decorators import handle_api_error
8
10
  from fiddler3.entities.base import BaseEntity
9
11
  from fiddler3.entities.helpers import raise_not_found
10
- from fiddler3.schemas.project import ProjectResponse
12
+ from fiddler3.schemas.filter_query import OperatorType, QueryCondition, QueryRule
13
+ from fiddler3.schemas.project import ProjectResp
14
+
15
+ if typing.TYPE_CHECKING:
16
+ from fiddler3.entities.model import ModelCompact
11
17
 
12
18
 
13
19
  class Project(BaseEntity):
14
20
  def __init__(self, name: str) -> None:
15
21
  """
16
22
  Construct a project instance
23
+
17
24
  :param name: Slug like name
18
25
  """
19
26
  self.name = name
@@ -23,7 +30,7 @@ class Project(BaseEntity):
23
30
  self.updated_at: datetime | None = None
24
31
 
25
32
  # Deserialized response object
26
- self._resp: ProjectResponse | None = None
33
+ self._resp: ProjectResp | None = None
27
34
 
28
35
  @staticmethod
29
36
  def _get_url(id_: UUID | str | None = None) -> str:
@@ -36,7 +43,7 @@ class Project(BaseEntity):
36
43
  """Build entity object from the given dictionary"""
37
44
 
38
45
  # Deserialize the response
39
- resp_obj = ProjectResponse(**data)
46
+ resp_obj = ProjectResp(**data)
40
47
 
41
48
  # Initialize
42
49
  instance = cls(
@@ -51,6 +58,22 @@ class Project(BaseEntity):
51
58
  instance._resp = resp_obj
52
59
  return instance
53
60
 
61
+ def _refresh(self, data: dict) -> None:
62
+ """Refresh the fields of this instance from the given response dictionary"""
63
+ # Deserialize the response
64
+ resp_obj = ProjectResp(**data)
65
+
66
+ fields = [
67
+ 'id',
68
+ 'name',
69
+ 'created_at',
70
+ 'updated_at',
71
+ ]
72
+ for field in fields:
73
+ setattr(self, field, getattr(resp_obj, field, None))
74
+
75
+ self._resp = resp_obj
76
+
54
77
  @classmethod
55
78
  @handle_api_error
56
79
  def get(cls, id_: UUID | str) -> Project:
@@ -62,7 +85,13 @@ class Project(BaseEntity):
62
85
  @handle_api_error
63
86
  def from_name(cls, name: str) -> Project:
64
87
  """Get the project instance using project name"""
65
- response = cls._client().get(url=cls._get_url(), params={'name': name})
88
+ _filter = QueryCondition(
89
+ rules=[QueryRule(field='name', operator=OperatorType.EQUAL, value=name)]
90
+ )
91
+
92
+ response = cls._client().get(
93
+ url=cls._get_url(), params={'filter': _filter.json()}
94
+ )
66
95
  if response.json()['data']['total'] == 0:
67
96
  raise_not_found('Project not found for the given identifier')
68
97
 
@@ -71,9 +100,7 @@ class Project(BaseEntity):
71
100
  @classmethod
72
101
  @handle_api_error
73
102
  def list(cls) -> Iterator[Project]:
74
- """
75
- Get a list of all projects in the organization
76
- """
103
+ """Get a list of all projects in the organization"""
77
104
  for project in cls._paginate(url=cls._get_url()):
78
105
  yield cls._from_dict(data=project)
79
106
 
@@ -81,10 +108,44 @@ class Project(BaseEntity):
81
108
  def create(self) -> Project:
82
109
  """Create a new project"""
83
110
  response = self._client().post(url=self._get_url(), data={'name': self.name})
84
- return self._from_response(response=response)
111
+ self._refresh_from_response(response=response)
112
+ return self
85
113
 
86
114
  @handle_api_error
87
115
  def delete(self) -> None:
88
116
  """Delete project"""
89
117
  # @TODO - Switch this to v3 once we have the endpoint
90
118
  self._client().delete(url=f'/v2/projects/{self.id}')
119
+
120
+ @property
121
+ def models(self) -> Iterator[ModelCompact]:
122
+ """Fetch all the models of this project"""
123
+ from fiddler3.entities.model import ( # pylint: disable=import-outside-toplevel
124
+ Model,
125
+ )
126
+
127
+ yield from Model.list(project_id=self.id)
128
+
129
+
130
+ @dataclass
131
+ class ProjectCompact:
132
+ id: UUID
133
+ name: str
134
+
135
+ def fetch(self) -> Project:
136
+ """Fetch project instance"""
137
+ return Project.get(id_=self.id)
138
+
139
+
140
+ class ProjectCompactMixin:
141
+ @property
142
+ def project(self) -> ProjectCompact:
143
+ """Project instance"""
144
+ response = getattr(self, '_resp', None)
145
+ if not response or not hasattr(response, 'project'):
146
+ raise AttributeError(
147
+ 'This property is available only for objects generated from API '
148
+ 'response.'
149
+ )
150
+
151
+ return ProjectCompact(id=response.project.id, name=response.project.name)
@@ -0,0 +1,45 @@
1
+ from dataclasses import dataclass
2
+ from uuid import UUID
3
+
4
+
5
+ @dataclass
6
+ class UserCompact:
7
+ id: UUID
8
+ full_name: str
9
+ email: str
10
+
11
+
12
+ class CreatedByMixin:
13
+ @property
14
+ def created_by(self) -> UserCompact:
15
+ """Model instance"""
16
+ response = getattr(self, '_resp', None)
17
+ if not response or not hasattr(response, 'created_by'):
18
+ raise AttributeError(
19
+ 'This property is available only for objects generated from API '
20
+ 'response.'
21
+ )
22
+
23
+ return UserCompact(
24
+ id=response.created_by.id,
25
+ full_name=response.created_by.full_name,
26
+ email=response.created_by.email,
27
+ )
28
+
29
+
30
+ class UpdatedByMixin:
31
+ @property
32
+ def updated_by(self) -> UserCompact:
33
+ """Model instance"""
34
+ response = getattr(self, '_resp', None)
35
+ if not response or not hasattr(response, 'updated_by'):
36
+ raise AttributeError(
37
+ 'This property is available only for objects generated from API '
38
+ 'response.'
39
+ )
40
+
41
+ return UserCompact(
42
+ id=response.updated_by.id,
43
+ full_name=response.updated_by.full_name,
44
+ email=response.updated_by.email,
45
+ )