Skip to content

Commit dfa369e

Browse files
committed
Fix activities
1 parent 1584cb9 commit dfa369e

File tree

3 files changed

+125
-154
lines changed

3 files changed

+125
-154
lines changed

python/hopsworks_common/core/feature_store_activity_api.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,26 @@ def get_feature_group_activities(
4141
feature_store_id,
4242
"featuregroups",
4343
feature_group_id,
44-
"activities",
44+
"activity",
4545
]
4646

4747
query_params = {
4848
"limit": limit,
4949
"offset": offset,
50+
"expand": [
51+
"users",
52+
"commits",
53+
"jobs",
54+
"validationreport",
55+
"expectationsuite",
56+
"executions",
57+
"statistics",
58+
],
59+
"sortby": "timestamp:desc",
5060
}
5161
if activity_type is not None and len(activity_type) > 0:
5262
query_params["activityType"] = activity_type
5363

54-
response = _client._request("GET", path_params, query_params=query_params)
64+
response = _client._send_request("GET", path_params, query_params=query_params)
5565

5666
return fsa_mod.FeatureStoreActivity.from_response_json(response)

python/hopsworks_common/feature_store_activity.py

+81-49
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121
from enum import Enum
2222
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
2323

24+
import humps
25+
from hopsworks import execution as execution_mod
2426
from hopsworks_common import user as user_mod
2527
from hopsworks_common import util
28+
from hsfs import expectation_suite as es_mod
29+
from hsfs import validation_report as vr_mod
2630
from hsfs.core.constants import HAS_GREAT_EXPECTATIONS
2731

2832

@@ -31,8 +35,6 @@
3135
ExpectationSuite,
3236
ExpectationSuiteValidationResult,
3337
)
34-
from hsfs import expectation_suite as es_mod
35-
from hsfs import validation_report as vr_mod
3638

3739

3840
class FeatureStoreActivityType(Enum):
@@ -44,69 +46,67 @@ class FeatureStoreActivityType(Enum):
4446
COMMIT = "COMMIT"
4547

4648

47-
@dataclass(frozen=True, init=False, repr=False)
49+
@dataclass(init=False, repr=False)
4850
class FeatureStoreActivity:
4951
type: FeatureStoreActivityType
50-
metadata: str
5152
timestamp: int
52-
user: user_mod.User
53+
metadata: str
54+
user: Optional[user_mod.User]
5355
# optional fields depending on the activity type
5456
validation_report: Optional[
5557
Union[vr_mod.ValidationReport, ExpectationSuiteValidationResult]
5658
] = None
5759
expectation_suite: Optional[Union[es_mod.ExpectationSuite, ExpectationSuite]] = None
5860
commit: Optional[Dict[str, Union[str, int, float]]] = None
5961
statistics: Optional[Dict[str, Union[str, int, float]]] = None
62+
execution: Optional[execution_mod.Execution] = None
63+
execution_last_event_time: Optional[int] = None
6064
# internal fields
6165
id: int
6266
href: str
6367

6468
def __init__(
6569
self,
66-
type: FeatureStoreActivityType,
67-
metadata: str,
70+
type: str,
6871
timestamp: int,
69-
user: Dict[str, Any],
72+
metadata: Optional[str] = None,
73+
user: Optional[Dict[str, Any]] = None,
74+
expectation_suite: Optional[Dict[str, Any]] = None,
75+
validation_report: Optional[Dict[str, Any]] = None,
76+
commit: Optional[Dict[str, Union[str, int, float]]] = None,
77+
statistics: Optional[Dict[str, Union[str, int, float]]] = None,
78+
execution: Optional[Dict[str, Any]] = None,
79+
execution_last_event_time: Optional[int] = None,
7080
**kwargs,
7181
):
72-
self.type = type
73-
self.metadata = metadata
82+
self.type = FeatureStoreActivityType(type) if isinstance(type, str) else type
7483
self.timestamp = timestamp
75-
self.user = user_mod.User.from_response_json(user)
7684

7785
self.id = kwargs.get("id")
78-
self.href = kwargs.get("href", "")
86+
self.href = kwargs.get("href")
7987

80-
self.commit = None
81-
self.expectation_suite = None
82-
self.validation_report = None
83-
self.statistics = None
88+
self.user = user_mod.User.from_response_json(user) if user else None
89+
self.metadata = metadata
90+
self.commit = commit
91+
self.statistics = statistics
92+
self.execution = (
93+
execution_mod.Execution.from_response_json(execution) if execution else None
94+
)
95+
self.execution_last_event_time = execution_last_event_time
8496

85-
if self.type == FeatureStoreActivityType.VALIDATIONS:
86-
if HAS_GREAT_EXPECTATIONS:
87-
self.validation_report = ExpectationSuiteValidationResult(
88-
**kwargs.get("validation_report")
89-
)
90-
else:
91-
self.validation_report = vr_mod.ValidationReport(
92-
**kwargs.get("validation_report")
93-
)
94-
95-
if self.type == FeatureStoreActivityType.EXPECTATIONS:
97+
if self.type == FeatureStoreActivityType.VALIDATIONS and validation_report:
98+
self.validation_report = vr_mod.ValidationReport.from_response_json(
99+
validation_report
100+
)
96101
if HAS_GREAT_EXPECTATIONS:
97-
self.expectation_suite = ExpectationSuite(
98-
**kwargs.get("expectation_suite")
99-
)
100-
else:
101-
self.expectation_suite = es_mod.ExpectationSuite(
102-
**kwargs.get("expectation_suite")
103-
)
104-
105-
if self.type == FeatureStoreActivityType.COMMIT:
106-
self.commit = kwargs.get("commit")
102+
self.validation_report = self.validation_report.to_ge_type()
107103

108-
if self.statistics:
109-
self.statistics = kwargs.get("statistics")
104+
if self.type == FeatureStoreActivityType.EXPECTATIONS and expectation_suite:
105+
self.expectation_suite = es_mod.ExpectationSuite.from_response_json(
106+
expectation_suite
107+
)
108+
if HAS_GREAT_EXPECTATIONS:
109+
self.expectation_suite = self.expectation_suite.to_ge_type()
110110

111111
@classmethod
112112
def from_response_json(
@@ -117,35 +117,67 @@ def from_response_json(
117117
cls.from_response_json(activity) for activity in response_json["items"]
118118
]
119119
else:
120-
return cls(**response_json)
120+
return cls(**humps.decamelize(response_json))
121121

122122
def to_dict(self) -> Dict[str, Any]:
123-
json = {
123+
activity_dict = {
124124
"id": self.id,
125125
"type": self.type.value,
126126
"metadata": self.metadata,
127127
"timestamp": self.timestamp,
128-
"user": self.user.json(),
129128
}
129+
if self.user:
130+
activity_dict["user"] = self.user.to_dict()
130131
if self.validation_report:
131-
json["validation_report"] = self.validation_report.json()
132+
activity_dict["validation_report"] = (
133+
self.validation_report.to_dict()
134+
if hasattr(self.validation_report, "_id")
135+
else self.validation_report.to_json_dict()
136+
)
132137
if self.expectation_suite:
133-
json["expectation_suite"] = self.expectation_suite.json()
138+
activity_dict["expectation_suite"] = (
139+
self.expectation_suite.to_dict()
140+
if hasattr(self.expectation_suite, "_id")
141+
else self.expectation_suite.to_json_dict()
142+
)
134143
if self.commit:
135-
json["commit"] = self.commit
144+
activity_dict["commit"] = self.commit
136145
if self.statistics:
137-
json["statistics"] = self.statistics
146+
activity_dict["statistics"] = self.statistics
147+
if self.execution:
148+
activity_dict["execution"] = humps.decamelize(
149+
json.loads(self.execution.json())
150+
)
138151

139-
return json
152+
return activity_dict
140153

141154
def json(self) -> str:
142155
return json.dumps(self.to_dict(), cls=util.Encoder)
143156

144157
def __repr__(self):
145158
utc_human_readable = (
146159
datetime.datetime.fromtimestamp(
147-
self.timestamp, datetime.timezone.utc
160+
self.timestamp / 1000, datetime.timezone.utc
148161
).strftime(r"%Y-%m-%d %H:%M:%S")
149162
+ " UTC"
150163
)
151-
return f"Activity:\n{self.type},\n{self.metadata},\nat: {utc_human_readable},\nuser: {self.user}"
164+
the_string = f"Activity {self.type.value},"
165+
the_string += f" at: {utc_human_readable}"
166+
if self.user:
167+
the_string += f", by: {self.user.email}"
168+
if self.metadata:
169+
the_string += f"\n\t{self.metadata}"
170+
if self.execution:
171+
the_string += f"\n{self.execution.get_url()},"
172+
if self.validation_report:
173+
the_string += f"Validation {'succeeded' if self.validation_report.success else 'failed'}."
174+
if self.expectation_suite:
175+
the_string += (
176+
f"It has {len(self.expectation_suite.expectations)} expectations."
177+
)
178+
if self.statistics:
179+
the_string += f"\nComputed following statistics:\n{json.dumps(self.statistics, indent=2)}"
180+
if self.commit:
181+
the_string += f"\nData ingestion:\n{json.dumps(self.commit, indent=2)}"
182+
183+
return the_string

python/hopsworks_common/user.py

+32-103
Original file line numberDiff line numberDiff line change
@@ -17,122 +17,51 @@
1717
from __future__ import annotations
1818

1919
import json
20+
from dataclasses import asdict, dataclass
21+
from typing import Any, Dict, Optional
2022

2123
import humps
2224
from hopsworks_common import util
2325

2426

27+
@dataclass
2528
class User:
26-
def __init__(
27-
self,
28-
username=None,
29-
email=None,
30-
first_name=None,
31-
last_name=None,
32-
status=None,
33-
secret=None,
34-
chosen_password=None,
35-
repeated_password=None,
36-
tos=None,
37-
two_factor=None,
38-
tours_state=None,
39-
max_num_projects=None,
40-
test_user=None,
41-
user_account_type=None,
42-
num_active_projects=None,
43-
num_remaining_projects=None,
44-
firstname=None,
45-
lastname=None,
46-
**kwargs,
47-
):
48-
self._username = username
49-
self._email = email
50-
self._first_name = first_name or firstname
51-
self._last_name = last_name or last_name
52-
self._status = status
53-
self._secret = secret
54-
self._chosen_password = chosen_password
55-
self._repeated_password = repeated_password
56-
self._tos = tos
57-
self._two_factor = two_factor
58-
self._tours_state = tours_state
59-
self._max_num_projects = max_num_projects
60-
self._test_user = test_user
61-
self._user_account_type = user_account_type
62-
self._num_active_projects = num_active_projects
63-
self._num_remaining_projects = num_remaining_projects
29+
email: str
30+
first_name: str
31+
last_name: str
32+
href: Optional[str] = None
33+
username: Optional[str] = None
34+
status: Optional[str] = None
35+
secret: Optional[str] = None
36+
chosen_password: Optional[str] = None
37+
repeated_password: Optional[str] = None
38+
tos: Optional[bool] = None
39+
two_factor: Optional[bool] = None
40+
user_account_type: Optional[str] = None
41+
tours_state: Optional[bool] = None
42+
max_num_projects: Optional[int] = None
43+
test_user: Optional[bool] = None
44+
num_active_projects: Optional[int] = None
45+
num_remaining_projects: Optional[int] = None
6446

6547
@classmethod
66-
def from_response_json(cls, json_dict):
48+
def from_response_json(cls, json_dict: Optional[Dict[str, Any]]) -> Optional[User]:
6749
if json_dict:
6850
json_decamelized = humps.decamelize(json_dict)
51+
if "firstname" in json_decamelized.keys():
52+
json_decamelized["first_name"] = json_decamelized.pop("firstname")
53+
json_decamelized["last_name"] = json_decamelized.pop("lastname")
54+
# Remove keys that are not part of the dataclass
55+
for key in set(json_decamelized.keys()) - set(
56+
User.__dataclass_fields__.keys()
57+
):
58+
json_decamelized.pop(key)
6959
return cls(**json_decamelized)
7060
else:
7161
return None
7262

73-
def json(self):
63+
def json(self) -> str:
7464
return json.dumps(self, cls=util.Encoder)
7565

76-
@property
77-
def username(self):
78-
return self._username
79-
80-
@property
81-
def email(self):
82-
return self._email
83-
84-
@property
85-
def first_name(self):
86-
return self._first_name
87-
88-
@property
89-
def last_name(self):
90-
return self._last_name
91-
92-
@property
93-
def status(self):
94-
return self._status
95-
96-
@property
97-
def secret(self):
98-
return self._secret
99-
100-
@property
101-
def chosen_password(self):
102-
return self._chosen_password
103-
104-
@property
105-
def repeated_password(self):
106-
return self._repeated_password
107-
108-
@property
109-
def tos(self):
110-
return self._tos
111-
112-
@property
113-
def two_factor(self):
114-
return self._two_factor
115-
116-
@property
117-
def tours_state(self):
118-
return self._tours_state
119-
120-
@property
121-
def max_num_projects(self):
122-
return self._max_num_projects
123-
124-
@property
125-
def test_user(self):
126-
return self._test_user
127-
128-
@property
129-
def user_account_type(self):
130-
return self._user_account_type
131-
132-
@property
133-
def num_active_projects(self):
134-
return self._num_active_projects
135-
136-
@property
137-
def num_remaining_projects(self):
138-
return self._num_remaining_projects
66+
def to_dict(self) -> Dict[str, Any]:
67+
return asdict(self)

0 commit comments

Comments
 (0)