1717
1818import re
1919from typing import Dict , List , Tuple
20+ from unittest import mock
21+ from unittest .mock import call , patch
2022
23+ from google .api_core import operation as ga_operation
2124from google .cloud import aiplatform
2225from google .cloud .aiplatform import base
23- from feature_store_constants import _TEST_FG1_FM1_DESCRIPTION
24- from feature_store_constants import _TEST_FG1_FM1_ID
25- from feature_store_constants import _TEST_FG1_FM1_LABELS
26- from feature_store_constants import _TEST_FG1_FM1_PATH
27- from feature_store_constants import _TEST_FG1_ID
28- from feature_store_constants import _TEST_LOCATION
29- from feature_store_constants import _TEST_PROJECT
30- from feature_store_constants import _TEST_FG1_FM1_SCHEDULE_CONFIG
31- from feature_store_constants import _TEST_FG1_FM1_FEATURE_SELECTION_CONFIGS
26+
27+ from feature_store_constants import (
28+ _TEST_PROJECT ,
29+ _TEST_LOCATION ,
30+ _TEST_FG1_ID ,
31+ _TEST_FG1_FM1_DESCRIPTION ,
32+ _TEST_FG1_FM1_FEATURE_SELECTION_CONFIGS ,
33+ _TEST_FG1_FM1_ID ,
34+ _TEST_FG1_FM1_LABELS ,
35+ _TEST_FG1_FM1_PATH ,
36+ _TEST_FG1_FM1_SCHEDULE_CONFIG ,
37+ _TEST_FG1_FMJ1 ,
38+ _TEST_FG1_FMJ1_DESCRIPTION ,
39+ _TEST_FG1_FMJ1_ID ,
40+ _TEST_FG1_FMJ1_LABELS ,
41+ _TEST_FG1_FMJ_LIST ,
42+ _TEST_FG1_FMJ1_PATH ,
43+ _TEST_FG1_FMJ2_DESCRIPTION ,
44+ _TEST_FG1_FMJ2_LABELS ,
45+ _TEST_FG1_FMJ2_PATH ,
46+ )
3247from vertexai .resources .preview import FeatureMonitor
48+ from google .cloud .aiplatform_v1beta1 .services .feature_registry_service import (
49+ FeatureRegistryServiceClient ,
50+ )
51+ from google .cloud .aiplatform .compat import types
52+ from vertexai .resources .preview .feature_store import (
53+ feature_monitor ,
54+ )
3355import pytest
3456
3557
3658pytestmark = pytest .mark .usefixtures ("google_auth_mock" )
3759
3860
61+ @pytest .fixture
62+ def fm_logger_mock ():
63+ with patch .object (
64+ feature_monitor ._LOGGER ,
65+ "info" ,
66+ wraps = feature_monitor ._LOGGER .info ,
67+ ) as logger_mock :
68+ yield logger_mock
69+
70+
71+ @pytest .fixture
72+ def get_feature_monitor_job_mock ():
73+ with patch .object (
74+ FeatureRegistryServiceClient ,
75+ "get_feature_monitor_job" ,
76+ ) as get_fmj_mock :
77+ get_fmj_mock .return_value = _TEST_FG1_FMJ1
78+ yield get_fmj_mock
79+
80+
81+ @pytest .fixture
82+ def create_feature_monitor_job_mock ():
83+ with patch .object (
84+ FeatureRegistryServiceClient ,
85+ "create_feature_monitor_job" ,
86+ ) as create_feature_monitor_job_mock :
87+ create_feature_monitor_job_lro_mock = mock .Mock (ga_operation .Operation )
88+ create_feature_monitor_job_lro_mock .result .return_value = _TEST_FG1_FMJ1
89+ create_feature_monitor_job_mock .return_value = (
90+ create_feature_monitor_job_lro_mock
91+ )
92+ yield create_feature_monitor_job_mock
93+
94+
95+ @pytest .fixture
96+ def list_feature_monitor_jobs_mock ():
97+ with patch .object (
98+ FeatureRegistryServiceClient ,
99+ "list_feature_monitor_jobs" ,
100+ ) as list_feature_monitor_jobs_mock :
101+ list_feature_monitor_jobs_mock .return_value = _TEST_FG1_FMJ_LIST
102+ yield list_feature_monitor_jobs_mock
103+
104+
39105def feature_monitor_eq (
40106 feature_monitor_to_check : FeatureMonitor ,
41107 name : str ,
@@ -56,8 +122,27 @@ def feature_monitor_eq(
56122 assert feature_monitor_to_check .labels == labels
57123
58124
125+ def feature_monitor_job_eq (
126+ feature_monitor_job_to_check : FeatureMonitor .FeatureMonitorJob ,
127+ resource_name : str ,
128+ project : str ,
129+ location : str ,
130+ description : str ,
131+ labels : Dict [str , str ],
132+ ):
133+ """Check if a Feature Monitor Job has the appropriate values set."""
134+ assert feature_monitor_job_to_check .resource_name == resource_name
135+ assert feature_monitor_job_to_check .project == project
136+ assert feature_monitor_job_to_check .location == location
137+ assert feature_monitor_job_to_check .description == description
138+ assert feature_monitor_job_to_check .labels == labels
139+
140+
59141def test_init_with_feature_monitor_id_and_no_fg_id_raises_error ():
60- aiplatform .init (project = _TEST_PROJECT , location = _TEST_LOCATION )
142+ aiplatform .init (
143+ project = _TEST_PROJECT ,
144+ location = _TEST_LOCATION ,
145+ )
61146
62147 with pytest .raises (
63148 ValueError ,
@@ -70,7 +155,10 @@ def test_init_with_feature_monitor_id_and_no_fg_id_raises_error():
70155
71156
72157def test_init_with_feature_monitor_path_and_fg_id_raises_error ():
73- aiplatform .init (project = _TEST_PROJECT , location = _TEST_LOCATION )
158+ aiplatform .init (
159+ project = _TEST_PROJECT ,
160+ location = _TEST_LOCATION ,
161+ )
74162
75163 with pytest .raises (
76164 ValueError ,
@@ -80,13 +168,22 @@ def test_init_with_feature_monitor_path_and_fg_id_raises_error():
80168 "path, feature_group_id should not be specified."
81169 ),
82170 ):
83- FeatureMonitor (_TEST_FG1_FM1_PATH , feature_group_id = _TEST_FG1_ID )
171+ FeatureMonitor (
172+ _TEST_FG1_FM1_PATH ,
173+ feature_group_id = _TEST_FG1_ID ,
174+ )
84175
85176
86177def test_init_with_feature_monitor_id (get_feature_monitor_mock ):
87- aiplatform .init (project = _TEST_PROJECT , location = _TEST_LOCATION )
178+ aiplatform .init (
179+ project = _TEST_PROJECT ,
180+ location = _TEST_LOCATION ,
181+ )
88182
89- feature_monitor = FeatureMonitor (_TEST_FG1_FM1_ID , feature_group_id = _TEST_FG1_ID )
183+ feature_monitor = FeatureMonitor (
184+ _TEST_FG1_FM1_ID ,
185+ feature_group_id = _TEST_FG1_ID ,
186+ )
90187
91188 get_feature_monitor_mock .assert_called_once_with (
92189 name = _TEST_FG1_FM1_PATH ,
@@ -106,8 +203,11 @@ def test_init_with_feature_monitor_id(get_feature_monitor_mock):
106203 )
107204
108205
109- def test_init_with_feature_path (get_feature_monitor_mock ):
110- aiplatform .init (project = _TEST_PROJECT , location = _TEST_LOCATION )
206+ def test_init_with_feature_monitor_path (get_feature_monitor_mock ):
207+ aiplatform .init (
208+ project = _TEST_PROJECT ,
209+ location = _TEST_LOCATION ,
210+ )
111211
112212 feature_monitor = FeatureMonitor (_TEST_FG1_FM1_PATH )
113213
@@ -127,3 +227,155 @@ def test_init_with_feature_path(get_feature_monitor_mock):
127227 schedule_config = _TEST_FG1_FM1_SCHEDULE_CONFIG ,
128228 feature_selection_configs = _TEST_FG1_FM1_FEATURE_SELECTION_CONFIGS ,
129229 )
230+
231+
232+ def test_init_with_feature_monitor_job_path (get_feature_monitor_job_mock ):
233+ aiplatform .init (
234+ project = _TEST_PROJECT ,
235+ location = _TEST_LOCATION ,
236+ )
237+
238+ feature_monitor_job = FeatureMonitor .FeatureMonitorJob (_TEST_FG1_FMJ1_PATH )
239+
240+ get_feature_monitor_job_mock .assert_called_once_with (
241+ name = _TEST_FG1_FMJ1_PATH ,
242+ retry = base ._DEFAULT_RETRY ,
243+ )
244+
245+ feature_monitor_job_eq (
246+ feature_monitor_job ,
247+ resource_name = _TEST_FG1_FMJ1_PATH ,
248+ project = _TEST_PROJECT ,
249+ location = _TEST_LOCATION ,
250+ description = _TEST_FG1_FMJ1_DESCRIPTION ,
251+ labels = _TEST_FG1_FMJ1_LABELS ,
252+ )
253+
254+
255+ @pytest .mark .parametrize ("create_request_timeout" , [None , 1.0 ])
256+ def test_create_feature_monitor_job (
257+ get_feature_monitor_mock ,
258+ get_feature_monitor_job_mock ,
259+ create_feature_monitor_job_mock ,
260+ create_request_timeout ,
261+ fm_logger_mock ,
262+ ):
263+ aiplatform .init (
264+ project = _TEST_PROJECT ,
265+ location = _TEST_LOCATION ,
266+ )
267+
268+ fm = FeatureMonitor (
269+ _TEST_FG1_FM1_ID ,
270+ feature_group_id = _TEST_FG1_ID ,
271+ )
272+ feature_monitor_job = fm .create_feature_monitor_job (
273+ description = _TEST_FG1_FMJ1_DESCRIPTION ,
274+ labels = _TEST_FG1_FMJ1_LABELS ,
275+ create_request_timeout = create_request_timeout ,
276+ )
277+
278+ expected_feature_monitor_job = types .feature_monitor_job .FeatureMonitorJob (
279+ description = _TEST_FG1_FMJ1_DESCRIPTION ,
280+ labels = _TEST_FG1_FMJ1_LABELS ,
281+ )
282+ create_feature_monitor_job_mock .assert_called_once_with (
283+ parent = _TEST_FG1_FM1_PATH ,
284+ feature_monitor_job = expected_feature_monitor_job ,
285+ metadata = (),
286+ timeout = create_request_timeout ,
287+ )
288+
289+ feature_monitor_job_eq (
290+ feature_monitor_job ,
291+ resource_name = _TEST_FG1_FMJ1_PATH ,
292+ project = _TEST_PROJECT ,
293+ location = _TEST_LOCATION ,
294+ description = _TEST_FG1_FMJ1_DESCRIPTION ,
295+ labels = _TEST_FG1_FMJ1_LABELS ,
296+ )
297+
298+ fm_logger_mock .assert_has_calls (
299+ [
300+ call ("Creating FeatureMonitorJob" ),
301+ call (
302+ f"Create FeatureMonitorJob backing LRO:"
303+ f" { create_feature_monitor_job_mock .return_value .operation .name } "
304+ ),
305+ call (
306+ "FeatureMonitorJob created. Resource name:"
307+ " projects/test-project/locations/us-central1/featureGroups/"
308+ "my_fg1/featureMonitors/my_fg1_fm1/featureMonitorJobs/1234567890"
309+ ),
310+ call ("To use this FeatureMonitorJob in another session:" ),
311+ call (
312+ "feature_monitor_job = aiplatform.FeatureMonitorJob("
313+ "'projects/test-project/locations/us-central1/featureGroups/"
314+ "my_fg1/featureMonitors/my_fg1_fm1/featureMonitorJobs/1234567890')"
315+ ),
316+ ]
317+ )
318+
319+
320+ def test_get_feature_monitor_job (
321+ get_feature_monitor_mock , get_feature_monitor_job_mock
322+ ):
323+ aiplatform .init (
324+ project = _TEST_PROJECT ,
325+ location = _TEST_LOCATION ,
326+ )
327+
328+ fm = FeatureMonitor (
329+ _TEST_FG1_FM1_ID ,
330+ feature_group_id = _TEST_FG1_ID ,
331+ )
332+ feature_monitor_job = fm .get_feature_monitor_job (_TEST_FG1_FMJ1_ID )
333+
334+ get_feature_monitor_job_mock .assert_called_once_with (
335+ name = _TEST_FG1_FMJ1_PATH ,
336+ retry = base ._DEFAULT_RETRY ,
337+ )
338+
339+ feature_monitor_job_eq (
340+ feature_monitor_job ,
341+ resource_name = _TEST_FG1_FMJ1_PATH ,
342+ project = _TEST_PROJECT ,
343+ location = _TEST_LOCATION ,
344+ description = _TEST_FG1_FMJ1_DESCRIPTION ,
345+ labels = _TEST_FG1_FMJ1_LABELS ,
346+ )
347+
348+
349+ def test_list_feature_monitors_jobs (
350+ get_feature_monitor_mock , list_feature_monitor_jobs_mock
351+ ):
352+ aiplatform .init (
353+ project = _TEST_PROJECT ,
354+ location = _TEST_LOCATION ,
355+ )
356+
357+ feature_monitor_jobs = FeatureMonitor (
358+ _TEST_FG1_FM1_ID ,
359+ feature_group_id = _TEST_FG1_ID ,
360+ ).list_feature_monitor_jobs ()
361+
362+ list_feature_monitor_jobs_mock .assert_called_once_with (
363+ request = {"parent" : _TEST_FG1_FM1_PATH }
364+ )
365+ assert len (feature_monitor_jobs ) == len (_TEST_FG1_FMJ_LIST )
366+ feature_monitor_job_eq (
367+ feature_monitor_jobs [0 ],
368+ resource_name = _TEST_FG1_FMJ1_PATH ,
369+ project = _TEST_PROJECT ,
370+ location = _TEST_LOCATION ,
371+ description = _TEST_FG1_FMJ1_DESCRIPTION ,
372+ labels = _TEST_FG1_FMJ1_LABELS ,
373+ )
374+ feature_monitor_job_eq (
375+ feature_monitor_jobs [1 ],
376+ resource_name = _TEST_FG1_FMJ2_PATH ,
377+ project = _TEST_PROJECT ,
378+ location = _TEST_LOCATION ,
379+ description = _TEST_FG1_FMJ2_DESCRIPTION ,
380+ labels = _TEST_FG1_FMJ2_LABELS ,
381+ )
0 commit comments