gitlabds 2.4.3__tar.gz → 2.4.4__tar.gz
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.
- {gitlabds-2.4.3 → gitlabds-2.4.4}/PKG-INFO +22 -10
- {gitlabds-2.4.3 → gitlabds-2.4.4}/README.md +21 -9
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/insights.py +14 -2
- gitlabds-2.4.4/gitlabds/serving_features.py +484 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds.egg-info/PKG-INFO +22 -10
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds.egg-info/SOURCES.txt +1 -0
- gitlabds-2.4.4/tests/test_serving_features.py +162 -0
- gitlabds-2.4.3/gitlabds/serving_features.py +0 -430
- {gitlabds-2.4.3 → gitlabds-2.4.4}/LICENSE +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/__init__.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/baselines.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/config_generator.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/dummy.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/feature_descriptions.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/feature_reduction.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/memory_optimization.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/missing.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/model_evaluator.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/monitoring_metrics.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/outliers.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/record_benchmarking.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/snowflake_connectors.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/split_data.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds/trends.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds.egg-info/dependency_links.txt +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds.egg-info/requires.txt +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/gitlabds.egg-info/top_level.txt +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/pyproject.toml +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/setup.cfg +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/setup.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/tests/test_feature_reduction.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/tests/test_record_benchmarking.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/tests/test_snowflake_connectors.py +0 -0
- {gitlabds-2.4.3 → gitlabds-2.4.4}/tests/test_split_data.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitlabds
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.4
|
|
4
4
|
Summary: GitLab Data Science Tools
|
|
5
5
|
Author-email: Kevin Dietz <kdietz@gitlab.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -790,7 +790,13 @@ display(effects_df)
|
|
|
790
790
|
```
|
|
791
791
|
</details>
|
|
792
792
|
|
|
793
|
-
<details><summary> Prescriptions </summary>
|
|
793
|
+
<details><summary> Prescriptions (deprecated) </summary>
|
|
794
|
+
|
|
795
|
+
> **Deprecated:** `prescriptions` raises a `DeprecationWarning` and will be removed in a future
|
|
796
|
+
> release. Use `per_record_shap` and `per_record_counterfactuals` instead. Note that
|
|
797
|
+
> `per_record_shap` requires a tree-based model (e.g. LightGBM, XGBoost), so they are not a
|
|
798
|
+
> drop-in replacement for linear/logistic models. They also choose each move's direction from
|
|
799
|
+
> the model and size moves from peer-group quantiles rather than a fixed step.
|
|
794
800
|
|
|
795
801
|
#### Description
|
|
796
802
|
Return "actionable" prescriptions and explanatory insights for each scored record. Insights first list actionable prescriptions follow by explainatory insights. This approach is recommended or linear/logistic methodologies only. Caution should be used if using a black box approach, as manpulating more than one prescription at a time could change a record's model score in unintended ways.
|
|
@@ -1391,24 +1397,30 @@ Also worth knowing:
|
|
|
1391
1397
|
<details><summary> Serve Features </summary>
|
|
1392
1398
|
|
|
1393
1399
|
#### Description
|
|
1394
|
-
Serve features from Snowflake Feature Store with
|
|
1400
|
+
Serve features from Snowflake Feature Store with per-call UDF parameters and retry logic. Each call shadows the shared `{FV}_DYNAMIC` view with a `CREATE OR REPLACE TEMPORARY VIEW` of the same name, so the parameters apply only to your Snowpark session: callers on other sessions (including your own other notebooks) never see them, and the permanent view keeps its UDF defaults.
|
|
1401
|
+
|
|
1402
|
+
- **Schema:** view and UDF names are qualified with the feature view's database and schema, so a `USE SCHEMA` in your session doesn't change what is served.
|
|
1403
|
+
- **Redeploying in the same session:** the temporary views last until the session closes, so a session that serves and then redeploys the permanent `_DYNAMIC` view keeps reading its temporary view until the next `serve_features` call. Use a fresh session to read the redeployed defaults.
|
|
1404
|
+
- **Concurrency:** calls sharing one session object run one at a time. Open one session per thread for real parallelism.
|
|
1405
|
+
- **Feature-date check:** when `feature_date` is set, the served date is verified and a mismatch raises `ValueError`. With `spine_timestamp_col` and `include_feature_view_timestamp_col=True` the returned `{FV}_{version}_FEATURE_DATE` column is checked; otherwise the session's temporary view is queried before retrieval, which may run the whole UDF.
|
|
1406
|
+
- **Spine timestamps earlier than `feature_date`** match no feature rows in the point-in-time join and return all-null features. A warning is raised when this happens.
|
|
1395
1407
|
|
|
1396
|
-
`gitlabds.serve_features(session, feature_store, feature_views_dict, spine_df=None, feature_date=None, spine_timestamp_col=None, include_feature_view_timestamp_col=False, lookback_window_value=None, lookback_window_unit=None, lock_timeout_seconds=
|
|
1408
|
+
`gitlabds.serve_features(session, feature_store, feature_views_dict, spine_df=None, feature_date=None, spine_timestamp_col=None, include_feature_view_timestamp_col=False, lookback_window_value=None, lookback_window_unit=None, lock_timeout_seconds=None, max_fetch_retries=3, fetch_retry_backoff_seconds=30):`
|
|
1397
1409
|
|
|
1398
1410
|
#### Parameters:
|
|
1399
|
-
- **_session_** : Active Snowflake Session object
|
|
1411
|
+
- **_session_** : Active Snowflake Session object. Must be the session the FeatureStore was built with (`feature_store._session`); a different session raises `ValueError`, because retrieval could not see this call's temporary views.
|
|
1400
1412
|
- **_feature_store_** : Initialized FeatureStore instance
|
|
1401
1413
|
- **_feature_views_dict_** : Dictionary mapping feature view names to versions (e.g., {"sales_activities": "1.0"})
|
|
1402
1414
|
- **_spine_df_** : One of the following:
|
|
1403
1415
|
- SQL query string to generate the spine DataFrame
|
|
1404
1416
|
- pandas DataFrame (will be converted to Snowpark DataFrame)
|
|
1405
1417
|
- Snowpark DataFrame (used directly)
|
|
1406
|
-
- **_feature_date_** : Date for feature retrieval (e.g., '2025-04-08'). If None, uses UDF default.
|
|
1418
|
+
- **_feature_date_** : Date for feature retrieval (e.g., '2025-04-08'). If None, uses UDF default and skips the feature-date check.
|
|
1407
1419
|
- **_spine_timestamp_col_** : Name of timestamp column in spine DataFrame for point-in-time lookup. Used for when multiple feature_dates may be present in the feature view. If only one feature_date is present, can set value to None for faster execution.
|
|
1408
|
-
- **_include_feature_view_timestamp_col_** : Whether to include timestamp column from feature views in output. Default is False.
|
|
1420
|
+
- **_include_feature_view_timestamp_col_** : Whether to include timestamp column from feature views in output. Only takes effect with `spine_timestamp_col`. Default is False.
|
|
1409
1421
|
- **_lookback_window_value_** : Either a global integer value (e.g., 6) or a dict with feature-view-specific values
|
|
1410
1422
|
- **_lookback_window_unit_** : Either a global string value (e.g., 'months') or a dict with feature-view-specific values
|
|
1411
|
-
- **_lock_timeout_seconds_** :
|
|
1423
|
+
- **_lock_timeout_seconds_** : Deprecated and ignored; the Snowflake lock table is no longer used. Passing it raises a `DeprecationWarning`.
|
|
1412
1424
|
- **_max_fetch_retries_** : Maximum number of attempts for the result-fetch step (`retrieve_feature_values` + `to_arrow().to_pandas()`) when a transient network/streaming error occurs (e.g., `ChunkedEncodingError`, "Response ended prematurely"). Set to 1 to disable retries. Default is 3.
|
|
1413
1425
|
- **_fetch_retry_backoff_seconds_** : Base sleep duration between fetch retries. Sleep grows linearly with attempt number (`attempt * backoff`). With defaults (3 retries, 30s base), sleeps are 30s before attempt 2 and 60s before attempt 3. Default is 30.
|
|
1414
1426
|
|
|
@@ -1468,10 +1480,10 @@ features = gitlabds.serve_features(
|
|
|
1468
1480
|
|
|
1469
1481
|
</details>
|
|
1470
1482
|
|
|
1471
|
-
<details><summary> Clear Feature Serving Locks </summary>
|
|
1483
|
+
<details><summary> Clear Feature Serving Locks (deprecated) </summary>
|
|
1472
1484
|
|
|
1473
1485
|
#### Description
|
|
1474
|
-
|
|
1486
|
+
**Deprecated.** `serve_features` no longer uses the `FEATURE_SERVING_LOCKS` table, so there is nothing to get stuck. This clears any leftover rows from that table, raises a `DeprecationWarning`, and will be removed in a future release. The table can be dropped.
|
|
1475
1487
|
|
|
1476
1488
|
`gitlabds.clear_feature_serving_locks(session):`
|
|
1477
1489
|
|
|
@@ -754,7 +754,13 @@ display(effects_df)
|
|
|
754
754
|
```
|
|
755
755
|
</details>
|
|
756
756
|
|
|
757
|
-
<details><summary> Prescriptions </summary>
|
|
757
|
+
<details><summary> Prescriptions (deprecated) </summary>
|
|
758
|
+
|
|
759
|
+
> **Deprecated:** `prescriptions` raises a `DeprecationWarning` and will be removed in a future
|
|
760
|
+
> release. Use `per_record_shap` and `per_record_counterfactuals` instead. Note that
|
|
761
|
+
> `per_record_shap` requires a tree-based model (e.g. LightGBM, XGBoost), so they are not a
|
|
762
|
+
> drop-in replacement for linear/logistic models. They also choose each move's direction from
|
|
763
|
+
> the model and size moves from peer-group quantiles rather than a fixed step.
|
|
758
764
|
|
|
759
765
|
#### Description
|
|
760
766
|
Return "actionable" prescriptions and explanatory insights for each scored record. Insights first list actionable prescriptions follow by explainatory insights. This approach is recommended or linear/logistic methodologies only. Caution should be used if using a black box approach, as manpulating more than one prescription at a time could change a record's model score in unintended ways.
|
|
@@ -1355,24 +1361,30 @@ Also worth knowing:
|
|
|
1355
1361
|
<details><summary> Serve Features </summary>
|
|
1356
1362
|
|
|
1357
1363
|
#### Description
|
|
1358
|
-
Serve features from Snowflake Feature Store with
|
|
1364
|
+
Serve features from Snowflake Feature Store with per-call UDF parameters and retry logic. Each call shadows the shared `{FV}_DYNAMIC` view with a `CREATE OR REPLACE TEMPORARY VIEW` of the same name, so the parameters apply only to your Snowpark session: callers on other sessions (including your own other notebooks) never see them, and the permanent view keeps its UDF defaults.
|
|
1365
|
+
|
|
1366
|
+
- **Schema:** view and UDF names are qualified with the feature view's database and schema, so a `USE SCHEMA` in your session doesn't change what is served.
|
|
1367
|
+
- **Redeploying in the same session:** the temporary views last until the session closes, so a session that serves and then redeploys the permanent `_DYNAMIC` view keeps reading its temporary view until the next `serve_features` call. Use a fresh session to read the redeployed defaults.
|
|
1368
|
+
- **Concurrency:** calls sharing one session object run one at a time. Open one session per thread for real parallelism.
|
|
1369
|
+
- **Feature-date check:** when `feature_date` is set, the served date is verified and a mismatch raises `ValueError`. With `spine_timestamp_col` and `include_feature_view_timestamp_col=True` the returned `{FV}_{version}_FEATURE_DATE` column is checked; otherwise the session's temporary view is queried before retrieval, which may run the whole UDF.
|
|
1370
|
+
- **Spine timestamps earlier than `feature_date`** match no feature rows in the point-in-time join and return all-null features. A warning is raised when this happens.
|
|
1359
1371
|
|
|
1360
|
-
`gitlabds.serve_features(session, feature_store, feature_views_dict, spine_df=None, feature_date=None, spine_timestamp_col=None, include_feature_view_timestamp_col=False, lookback_window_value=None, lookback_window_unit=None, lock_timeout_seconds=
|
|
1372
|
+
`gitlabds.serve_features(session, feature_store, feature_views_dict, spine_df=None, feature_date=None, spine_timestamp_col=None, include_feature_view_timestamp_col=False, lookback_window_value=None, lookback_window_unit=None, lock_timeout_seconds=None, max_fetch_retries=3, fetch_retry_backoff_seconds=30):`
|
|
1361
1373
|
|
|
1362
1374
|
#### Parameters:
|
|
1363
|
-
- **_session_** : Active Snowflake Session object
|
|
1375
|
+
- **_session_** : Active Snowflake Session object. Must be the session the FeatureStore was built with (`feature_store._session`); a different session raises `ValueError`, because retrieval could not see this call's temporary views.
|
|
1364
1376
|
- **_feature_store_** : Initialized FeatureStore instance
|
|
1365
1377
|
- **_feature_views_dict_** : Dictionary mapping feature view names to versions (e.g., {"sales_activities": "1.0"})
|
|
1366
1378
|
- **_spine_df_** : One of the following:
|
|
1367
1379
|
- SQL query string to generate the spine DataFrame
|
|
1368
1380
|
- pandas DataFrame (will be converted to Snowpark DataFrame)
|
|
1369
1381
|
- Snowpark DataFrame (used directly)
|
|
1370
|
-
- **_feature_date_** : Date for feature retrieval (e.g., '2025-04-08'). If None, uses UDF default.
|
|
1382
|
+
- **_feature_date_** : Date for feature retrieval (e.g., '2025-04-08'). If None, uses UDF default and skips the feature-date check.
|
|
1371
1383
|
- **_spine_timestamp_col_** : Name of timestamp column in spine DataFrame for point-in-time lookup. Used for when multiple feature_dates may be present in the feature view. If only one feature_date is present, can set value to None for faster execution.
|
|
1372
|
-
- **_include_feature_view_timestamp_col_** : Whether to include timestamp column from feature views in output. Default is False.
|
|
1384
|
+
- **_include_feature_view_timestamp_col_** : Whether to include timestamp column from feature views in output. Only takes effect with `spine_timestamp_col`. Default is False.
|
|
1373
1385
|
- **_lookback_window_value_** : Either a global integer value (e.g., 6) or a dict with feature-view-specific values
|
|
1374
1386
|
- **_lookback_window_unit_** : Either a global string value (e.g., 'months') or a dict with feature-view-specific values
|
|
1375
|
-
- **_lock_timeout_seconds_** :
|
|
1387
|
+
- **_lock_timeout_seconds_** : Deprecated and ignored; the Snowflake lock table is no longer used. Passing it raises a `DeprecationWarning`.
|
|
1376
1388
|
- **_max_fetch_retries_** : Maximum number of attempts for the result-fetch step (`retrieve_feature_values` + `to_arrow().to_pandas()`) when a transient network/streaming error occurs (e.g., `ChunkedEncodingError`, "Response ended prematurely"). Set to 1 to disable retries. Default is 3.
|
|
1377
1389
|
- **_fetch_retry_backoff_seconds_** : Base sleep duration between fetch retries. Sleep grows linearly with attempt number (`attempt * backoff`). With defaults (3 retries, 30s base), sleeps are 30s before attempt 2 and 60s before attempt 3. Default is 30.
|
|
1378
1390
|
|
|
@@ -1432,10 +1444,10 @@ features = gitlabds.serve_features(
|
|
|
1432
1444
|
|
|
1433
1445
|
</details>
|
|
1434
1446
|
|
|
1435
|
-
<details><summary> Clear Feature Serving Locks </summary>
|
|
1447
|
+
<details><summary> Clear Feature Serving Locks (deprecated) </summary>
|
|
1436
1448
|
|
|
1437
1449
|
#### Description
|
|
1438
|
-
|
|
1450
|
+
**Deprecated.** `serve_features` no longer uses the `FEATURE_SERVING_LOCKS` table, so there is nothing to get stuck. This clears any leftover rows from that table, raises a `DeprecationWarning`, and will be removed in a future release. The table can be dropped.
|
|
1439
1451
|
|
|
1440
1452
|
`gitlabds.clear_feature_serving_locks(session):`
|
|
1441
1453
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
import pandas as pd
|
|
2
4
|
import numpy as np
|
|
3
5
|
|
|
@@ -283,8 +285,18 @@ def prescriptions(model, input_df:pd.DataFrame, scored_df:pd.DataFrame, actionab
|
|
|
283
285
|
explanation_fields : List of explainable (non-actionable insights) fields to return insights for. Defaults to 'all' for all fields.
|
|
284
286
|
|
|
285
287
|
|
|
286
|
-
Returns a Dataframe of prescriptive actions. One row per record input.
|
|
287
|
-
|
|
288
|
+
Returns a Dataframe of prescriptive actions. One row per record input.
|
|
289
|
+
|
|
290
|
+
Deprecated: use per_record_shap and per_record_counterfactuals instead. prescriptions will
|
|
291
|
+
be removed in a future release."""
|
|
292
|
+
|
|
293
|
+
warnings.warn(
|
|
294
|
+
"gitlabds.prescriptions is deprecated and will be removed in a future release. "
|
|
295
|
+
"Use gitlabds.per_record_shap and gitlabds.per_record_counterfactuals instead.",
|
|
296
|
+
DeprecationWarning,
|
|
297
|
+
stacklevel=2,
|
|
298
|
+
)
|
|
299
|
+
|
|
288
300
|
bf = pd.DataFrame(input_df.columns.tolist(), columns=['feature'])
|
|
289
301
|
|
|
290
302
|
change_score = pd.DataFrame()
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Feature serving functionality for Snowflake Feature Store integration.
|
|
3
|
+
|
|
4
|
+
This module serves features from Snowflake Feature Store. Each call applies its parameters
|
|
5
|
+
through session-scoped temporary views, so callers on different Snowpark sessions never see
|
|
6
|
+
each other's parameters, and calls sharing one session are serialized in-process.
|
|
7
|
+
|
|
8
|
+
Requires the optional Snowflake dependencies: pip install gitlabds[snowflake]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
import warnings
|
|
16
|
+
import weakref
|
|
17
|
+
from typing import Any, Dict, List, Optional, Union
|
|
18
|
+
|
|
19
|
+
import pandas as pd
|
|
20
|
+
from snowflake.snowpark import Session, DataFrame as SnowparkDataFrame
|
|
21
|
+
from snowflake.ml.feature_store import FeatureStore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_SESSION_LOCKS: "weakref.WeakKeyDictionary[Session, threading.Lock]" = weakref.WeakKeyDictionary()
|
|
25
|
+
_SESSION_LOCKS_GUARD = threading.Lock()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Transient network/streaming errors that warrant a retry of the result fetch.
|
|
29
|
+
# These are the errors that surface when the HTTP response from Snowflake is
|
|
30
|
+
# interrupted mid-stream (e.g., flaky network, VPN blip, load-balancer reset).
|
|
31
|
+
def _is_transient_fetch_error(exc: BaseException) -> bool:
|
|
32
|
+
"""Return True if the exception looks like a transient streaming/network failure."""
|
|
33
|
+
# Match by class name to avoid hard imports of requests / urllib3 at module load.
|
|
34
|
+
transient_class_names = {
|
|
35
|
+
"ChunkedEncodingError",
|
|
36
|
+
"ProtocolError",
|
|
37
|
+
"ConnectionError",
|
|
38
|
+
"ConnectionResetError",
|
|
39
|
+
"ReadTimeoutError",
|
|
40
|
+
"ReadTimeout",
|
|
41
|
+
"IncompleteRead",
|
|
42
|
+
}
|
|
43
|
+
# Walk the exception chain — Snowpark often wraps the original error.
|
|
44
|
+
seen = set()
|
|
45
|
+
current = exc
|
|
46
|
+
while current is not None and id(current) not in seen:
|
|
47
|
+
seen.add(id(current))
|
|
48
|
+
if type(current).__name__ in transient_class_names:
|
|
49
|
+
return True
|
|
50
|
+
msg = str(current).lower()
|
|
51
|
+
if "response ended prematurely" in msg:
|
|
52
|
+
return True
|
|
53
|
+
if "connection reset" in msg:
|
|
54
|
+
return True
|
|
55
|
+
if "connection aborted" in msg:
|
|
56
|
+
return True
|
|
57
|
+
current = current.__cause__ or current.__context__
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _session_lock(session: "Session") -> threading.Lock:
|
|
62
|
+
"""Return the process-local lock that serializes serve_features calls on one session."""
|
|
63
|
+
with _SESSION_LOCKS_GUARD:
|
|
64
|
+
return _SESSION_LOCKS.setdefault(session, threading.Lock())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _to_date(value: Any) -> datetime.date:
|
|
68
|
+
"""Normalize a date-like value (str, date, datetime, Timestamp) to a datetime.date."""
|
|
69
|
+
return pd.Timestamp(value).date()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_dynamic_view_feature_date(
|
|
73
|
+
session: "Session",
|
|
74
|
+
dynamic_view: str,
|
|
75
|
+
timestamp_col: str,
|
|
76
|
+
feature_date: str,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Raise if the session's dynamic view does not return the requested feature_date."""
|
|
79
|
+
rows = session.sql(f"SELECT {timestamp_col} FROM {dynamic_view} LIMIT 1").collect()
|
|
80
|
+
if not rows:
|
|
81
|
+
warnings.warn(
|
|
82
|
+
f"{dynamic_view} returned no rows; could not verify feature_date {feature_date}"
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
actual = rows[0][0]
|
|
86
|
+
if actual is None or _to_date(actual) != _to_date(feature_date):
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"{dynamic_view} returned {timestamp_col} {actual}, expected {feature_date}. "
|
|
89
|
+
f"The session's temporary view is not serving the requested parameters."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _check_retrieved_feature_date(
|
|
94
|
+
features_df: pd.DataFrame,
|
|
95
|
+
feature_date_col: str,
|
|
96
|
+
feature_date: str,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Raise if a retrieved feature-date column holds any date other than feature_date."""
|
|
99
|
+
values = features_df[feature_date_col].dropna()
|
|
100
|
+
if values.empty:
|
|
101
|
+
warnings.warn(
|
|
102
|
+
f"{feature_date_col} is null for every row, so all features from that view are "
|
|
103
|
+
f"null. A spine timestamp earlier than feature_date {feature_date} matches no "
|
|
104
|
+
f"feature rows in the point-in-time join."
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
expected = _to_date(feature_date)
|
|
108
|
+
mismatched = sorted({d for d in values.map(_to_date) if d != expected})
|
|
109
|
+
if mismatched:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"{feature_date_col} returned {mismatched}, expected {feature_date}. "
|
|
112
|
+
f"The session's temporary view is not serving the requested parameters."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _warn_spine_before_feature_date(
|
|
117
|
+
features_df: pd.DataFrame,
|
|
118
|
+
spine_timestamp_col: str,
|
|
119
|
+
feature_date: str,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Warn when spine timestamps precede feature_date, which yields all-null features."""
|
|
122
|
+
matches = [c for c in features_df.columns if c.upper() == spine_timestamp_col.upper()]
|
|
123
|
+
if not matches or features_df.empty:
|
|
124
|
+
return
|
|
125
|
+
spine_ts = pd.to_datetime(features_df[matches[0]])
|
|
126
|
+
if getattr(spine_ts.dt, "tz", None) is not None:
|
|
127
|
+
spine_ts = spine_ts.dt.tz_localize(None)
|
|
128
|
+
n_early = int((spine_ts.dt.normalize() < pd.Timestamp(feature_date)).sum())
|
|
129
|
+
if n_early:
|
|
130
|
+
pct = n_early / len(features_df)
|
|
131
|
+
warnings.warn(
|
|
132
|
+
f"{n_early} spine rows ({pct:.2%}) have {spine_timestamp_col} earlier than "
|
|
133
|
+
f"feature_date {feature_date}. The point-in-time join returns null features "
|
|
134
|
+
f"for those rows."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def clear_feature_serving_locks(session: "Session") -> int:
|
|
139
|
+
"""
|
|
140
|
+
Clear rows from the legacy FEATURE_SERVING_LOCKS table.
|
|
141
|
+
|
|
142
|
+
Deprecated: serve_features no longer uses a Snowflake lock table, so there is nothing
|
|
143
|
+
to get stuck. This function will be removed in a future release, and the
|
|
144
|
+
FEATURE_SERVING_LOCKS table can be dropped.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
session : snowflake.snowpark.Session
|
|
149
|
+
Active Snowflake Session object.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
int
|
|
154
|
+
Number of lock rows cleared, or 0 if none exist or the table is gone.
|
|
155
|
+
"""
|
|
156
|
+
warnings.warn(
|
|
157
|
+
"gitlabds.clear_feature_serving_locks is deprecated and will be removed in a future "
|
|
158
|
+
"release. serve_features no longer uses the FEATURE_SERVING_LOCKS table.",
|
|
159
|
+
DeprecationWarning,
|
|
160
|
+
stacklevel=2,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
lock_table = "FEATURE_SERVING_LOCKS"
|
|
164
|
+
lock_name = "FEATURE_SERVING"
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
existing_locks = session.sql(f"""
|
|
168
|
+
SELECT session_id, acquired_at, expires_at,
|
|
169
|
+
CASE WHEN expires_at > CURRENT_TIMESTAMP() THEN 'ACTIVE' ELSE 'EXPIRED' END as status
|
|
170
|
+
FROM {lock_table}
|
|
171
|
+
WHERE lock_name = '{lock_name}'
|
|
172
|
+
ORDER BY acquired_at DESC
|
|
173
|
+
""").collect()
|
|
174
|
+
|
|
175
|
+
if existing_locks:
|
|
176
|
+
print("🔍 Current feature serving locks:")
|
|
177
|
+
for lock in existing_locks:
|
|
178
|
+
print(f" Session: {lock['SESSION_ID']}, "
|
|
179
|
+
f"Acquired: {lock['ACQUIRED_AT']}, "
|
|
180
|
+
f"Expires: {lock['EXPIRES_AT']}, "
|
|
181
|
+
f"Status: {lock['STATUS']}")
|
|
182
|
+
else:
|
|
183
|
+
print("✅ No existing locks found")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
session.sql(f"""
|
|
187
|
+
DELETE FROM {lock_table}
|
|
188
|
+
WHERE lock_name = '{lock_name}'
|
|
189
|
+
""").collect()
|
|
190
|
+
|
|
191
|
+
locks_cleared = len(existing_locks)
|
|
192
|
+
print(f"🧹 Cleared {locks_cleared} feature serving locks")
|
|
193
|
+
return locks_cleared
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"❌ Error clearing locks: {e}")
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _udf_params(
|
|
201
|
+
fv_name: str,
|
|
202
|
+
feature_date: Optional[str],
|
|
203
|
+
lookback_window_value: Optional[Union[int, Dict[str, int]]],
|
|
204
|
+
lookback_window_unit: Optional[Union[str, Dict[str, str]]],
|
|
205
|
+
) -> List[str]:
|
|
206
|
+
"""Build the positional UDF arguments: SNAPSHOT_DATE, LOOKBACK_WINDOW_VALUE, LOOKBACK_WINDOW_UNIT."""
|
|
207
|
+
udf_params = []
|
|
208
|
+
|
|
209
|
+
if feature_date:
|
|
210
|
+
udf_params.append(f"'{feature_date}'::DATE")
|
|
211
|
+
print(f" Using snapshot date: {feature_date}")
|
|
212
|
+
else:
|
|
213
|
+
print(" Using UDF default snapshot date")
|
|
214
|
+
|
|
215
|
+
if isinstance(lookback_window_value, dict) and fv_name in lookback_window_value:
|
|
216
|
+
udf_params.append(str(lookback_window_value[fv_name]))
|
|
217
|
+
print(f" Using per-UDF lookback window value: {lookback_window_value[fv_name]}")
|
|
218
|
+
elif lookback_window_value is not None and not isinstance(lookback_window_value, dict):
|
|
219
|
+
udf_params.append(str(lookback_window_value))
|
|
220
|
+
print(f" Using global lookback window value: {lookback_window_value}")
|
|
221
|
+
else:
|
|
222
|
+
print(" Using UDF default lookback window value")
|
|
223
|
+
|
|
224
|
+
if isinstance(lookback_window_unit, dict) and fv_name in lookback_window_unit:
|
|
225
|
+
udf_params.append(f"'{lookback_window_unit[fv_name]}'")
|
|
226
|
+
print(f" Using per-UDF lookback window unit: {lookback_window_unit[fv_name]}")
|
|
227
|
+
elif lookback_window_unit is not None and not isinstance(lookback_window_unit, dict):
|
|
228
|
+
udf_params.append(f"'{lookback_window_unit}'")
|
|
229
|
+
print(f" Using global lookback window unit: {lookback_window_unit}")
|
|
230
|
+
else:
|
|
231
|
+
print(" Using UDF default lookback window unit")
|
|
232
|
+
|
|
233
|
+
return udf_params
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def serve_features(
|
|
237
|
+
session: "Session",
|
|
238
|
+
feature_store: "FeatureStore",
|
|
239
|
+
feature_views_dict: Dict[str, str],
|
|
240
|
+
spine_df: Optional[Union[str, pd.DataFrame, "SnowparkDataFrame"]] = None,
|
|
241
|
+
feature_date: Optional[str] = None,
|
|
242
|
+
spine_timestamp_col: Optional[str] = None,
|
|
243
|
+
include_feature_view_timestamp_col: bool = False,
|
|
244
|
+
lookback_window_value: Optional[Union[int, Dict[str, int]]] = None,
|
|
245
|
+
lookback_window_unit: Optional[Union[str, Dict[str, str]]] = None,
|
|
246
|
+
lock_timeout_seconds: Optional[int] = None,
|
|
247
|
+
max_fetch_retries: int = 3,
|
|
248
|
+
fetch_retry_backoff_seconds: int = 30,
|
|
249
|
+
) -> pd.DataFrame:
|
|
250
|
+
"""
|
|
251
|
+
Serve features from Snowflake Feature Store with per-call UDF parameters.
|
|
252
|
+
|
|
253
|
+
Each feature view reads from a ``{FV}_DYNAMIC`` view. This function shadows that view
|
|
254
|
+
with a ``CREATE OR REPLACE TEMPORARY VIEW`` of the same name that calls ``{FV}_UDF``
|
|
255
|
+
with this call's parameters. Temporary views are scoped to the Snowpark session, so
|
|
256
|
+
callers on different sessions (including the same user in two notebooks) never see
|
|
257
|
+
each other's parameters, and the permanent ``_DYNAMIC`` view keeps its UDF defaults.
|
|
258
|
+
View and UDF names are qualified with the feature view's database and schema, so the
|
|
259
|
+
session's current schema does not matter.
|
|
260
|
+
|
|
261
|
+
The temporary views last until the session closes. A session that serves features and
|
|
262
|
+
then redeploys the permanent ``_DYNAMIC`` view keeps reading its temporary view until
|
|
263
|
+
the next ``serve_features`` call; use a fresh session to read the redeployed defaults.
|
|
264
|
+
|
|
265
|
+
Calls sharing one session object run one at a time: a process-local lock per session
|
|
266
|
+
is held for the whole call. Open one session per thread for real parallelism.
|
|
267
|
+
|
|
268
|
+
When ``feature_date`` is set, the served feature date is verified. With
|
|
269
|
+
``spine_timestamp_col`` and ``include_feature_view_timestamp_col=True`` the returned
|
|
270
|
+
``{FV}_{version}_{timestamp_col}`` column is checked; otherwise the session's temporary view is
|
|
271
|
+
queried before retrieval (``SELECT {timestamp_col} ... LIMIT 1``), which may run the
|
|
272
|
+
whole UDF.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
session : snowflake.snowpark.Session
|
|
277
|
+
Active Snowflake Session object. Must be the session ``feature_store`` was built
|
|
278
|
+
with (``feature_store._session``): retrieval reads through that session, so temp
|
|
279
|
+
views created on any other session are invisible to it.
|
|
280
|
+
feature_store : snowflake.ml.feature_store.FeatureStore
|
|
281
|
+
Initialized FeatureStore instance.
|
|
282
|
+
feature_views_dict : Dict[str, str]
|
|
283
|
+
Mapping of feature view names to versions (e.g., {"sales_activities": "1.0"}).
|
|
284
|
+
spine_df : Union[str, pd.DataFrame, snowflake.snowpark.DataFrame]
|
|
285
|
+
SQL query string, pandas DataFrame (converted to Snowpark), or Snowpark DataFrame.
|
|
286
|
+
Required by the Feature Store SDK; None raises ValueError.
|
|
287
|
+
feature_date : str, optional
|
|
288
|
+
Snapshot date for features (e.g., '2025-04-08'). If None, uses the UDF default and
|
|
289
|
+
the feature-date check is skipped.
|
|
290
|
+
spine_timestamp_col : str, optional
|
|
291
|
+
Name of the timestamp column in the spine for point-in-time lookup. If None,
|
|
292
|
+
point-in-time lookup is disabled. Spine timestamps earlier than ``feature_date``
|
|
293
|
+
match no feature rows and return null features; a warning is raised when that
|
|
294
|
+
happens.
|
|
295
|
+
include_feature_view_timestamp_col : bool, default=False
|
|
296
|
+
Include each feature view's timestamp column in the output. Only takes effect with
|
|
297
|
+
``spine_timestamp_col``.
|
|
298
|
+
lookback_window_value : Union[int, Dict[str, int]], optional
|
|
299
|
+
Global integer (e.g., 6) or per-feature-view dict
|
|
300
|
+
(e.g., {"sales_activities_by_account": 3}).
|
|
301
|
+
lookback_window_unit : Union[str, Dict[str, str]], optional
|
|
302
|
+
Global string (e.g., 'months') or per-feature-view dict
|
|
303
|
+
(e.g., {"sales_activities_by_account": 'days'}).
|
|
304
|
+
lock_timeout_seconds : int, optional
|
|
305
|
+
Deprecated and ignored. The Snowflake lock table is no longer used.
|
|
306
|
+
max_fetch_retries : int, default=3
|
|
307
|
+
Maximum attempts for the result fetch (retrieve_feature_values +
|
|
308
|
+
to_arrow().to_pandas()) on transient network/streaming errors. 1 disables retries.
|
|
309
|
+
fetch_retry_backoff_seconds : int, default=30
|
|
310
|
+
Base sleep between fetch retries; grows linearly (attempt * backoff).
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
pd.DataFrame
|
|
315
|
+
Combined features from all feature views, with lowercase column names.
|
|
316
|
+
|
|
317
|
+
Raises
|
|
318
|
+
------
|
|
319
|
+
TypeError
|
|
320
|
+
When spine_df or feature_views_dict has the wrong type.
|
|
321
|
+
ValueError
|
|
322
|
+
When spine_df is None, feature_views_dict is empty, session is not the feature
|
|
323
|
+
store's session, or the served feature date does not match ``feature_date``.
|
|
324
|
+
|
|
325
|
+
Examples
|
|
326
|
+
--------
|
|
327
|
+
>>> df = serve_features(session, fs, {"sales_activities": "1.0"},
|
|
328
|
+
... spine_df="SELECT account_id FROM accounts",
|
|
329
|
+
... feature_date="2025-04-08")
|
|
330
|
+
"""
|
|
331
|
+
if not isinstance(feature_views_dict, dict):
|
|
332
|
+
raise TypeError(f"feature_views_dict must be a dict, got {type(feature_views_dict)}")
|
|
333
|
+
if not feature_views_dict:
|
|
334
|
+
raise ValueError("feature_views_dict is empty")
|
|
335
|
+
if spine_df is None:
|
|
336
|
+
raise ValueError("spine_df is required - the Snowflake Feature Store SDK requires a spine DataFrame")
|
|
337
|
+
if not isinstance(spine_df, (str, pd.DataFrame, SnowparkDataFrame)):
|
|
338
|
+
raise TypeError(
|
|
339
|
+
f"spine_df must be a SQL query string, pandas DataFrame, or Snowpark DataFrame, "
|
|
340
|
+
f"got {type(spine_df)}"
|
|
341
|
+
)
|
|
342
|
+
if lock_timeout_seconds is not None:
|
|
343
|
+
warnings.warn(
|
|
344
|
+
"lock_timeout_seconds is deprecated and ignored: serve_features no longer uses a "
|
|
345
|
+
"Snowflake lock. It will be removed in a future release.",
|
|
346
|
+
DeprecationWarning,
|
|
347
|
+
stacklevel=2,
|
|
348
|
+
)
|
|
349
|
+
fs_session = getattr(feature_store, "_session", None)
|
|
350
|
+
if fs_session is None:
|
|
351
|
+
warnings.warn(
|
|
352
|
+
"Could not read feature_store._session to confirm it is the session passed to "
|
|
353
|
+
"serve_features. If they differ, retrieval cannot see this call's temporary "
|
|
354
|
+
"views and returns UDF-default features."
|
|
355
|
+
)
|
|
356
|
+
elif fs_session is not session:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
"session must be the session feature_store was built with (feature_store._session). "
|
|
359
|
+
"Retrieval reads through the feature store's session, so temporary views created "
|
|
360
|
+
"on a different session are invisible to it and UDF-default features would be "
|
|
361
|
+
"returned. Pass session=feature_store._session."
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
call_id = f"call_{uuid.uuid4().hex[:8]}"
|
|
365
|
+
check_returned_column = (
|
|
366
|
+
bool(feature_date)
|
|
367
|
+
and spine_timestamp_col is not None
|
|
368
|
+
and include_feature_view_timestamp_col
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
lock = _session_lock(session)
|
|
372
|
+
if not lock.acquire(blocking=False):
|
|
373
|
+
print(f"⏳ [{call_id}] Waiting for another serve_features call on this session")
|
|
374
|
+
lock.acquire()
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
feature_views_list = []
|
|
378
|
+
feature_date_cols = {}
|
|
379
|
+
|
|
380
|
+
for fv_name, version in feature_views_dict.items():
|
|
381
|
+
fv = feature_store.get_feature_view(fv_name, version)
|
|
382
|
+
feature_views_list.append(fv)
|
|
383
|
+
|
|
384
|
+
fv_schema = f"{fv.database}.{fv.schema}"
|
|
385
|
+
dynamic_view = f"{fv_schema}.{fv_name.upper()}_DYNAMIC"
|
|
386
|
+
udf_name = f"{fv_schema}.{fv_name.upper()}_UDF"
|
|
387
|
+
entity_id = fv.entities[0].join_keys[0]
|
|
388
|
+
timestamp_col = fv.timestamp_col
|
|
389
|
+
|
|
390
|
+
print(f"[{call_id}] {fv_name} v{version}:")
|
|
391
|
+
print(f" Dynamic View: {dynamic_view} (temporary)")
|
|
392
|
+
print(f" UDF: {udf_name}")
|
|
393
|
+
print(f" Entity: {entity_id}")
|
|
394
|
+
print(f" Timestamp: {timestamp_col}")
|
|
395
|
+
|
|
396
|
+
params_str = ", ".join(
|
|
397
|
+
_udf_params(fv_name, feature_date, lookback_window_value, lookback_window_unit)
|
|
398
|
+
)
|
|
399
|
+
update_view_sql = f"""
|
|
400
|
+
CREATE OR REPLACE TEMPORARY VIEW {dynamic_view} AS
|
|
401
|
+
SELECT * FROM TABLE({udf_name}({params_str}))
|
|
402
|
+
"""
|
|
403
|
+
print(f" {update_view_sql.strip()}")
|
|
404
|
+
session.sql(update_view_sql).collect()
|
|
405
|
+
print(f" [{call_id}] Updated {dynamic_view}")
|
|
406
|
+
|
|
407
|
+
if not feature_date:
|
|
408
|
+
continue
|
|
409
|
+
if not timestamp_col:
|
|
410
|
+
warnings.warn(
|
|
411
|
+
f"{fv_name} has no timestamp_col; could not verify feature_date {feature_date}"
|
|
412
|
+
)
|
|
413
|
+
elif check_returned_column:
|
|
414
|
+
feature_date_cols[f"{fv_name}_{version}_{timestamp_col}".upper()] = (
|
|
415
|
+
dynamic_view,
|
|
416
|
+
timestamp_col,
|
|
417
|
+
)
|
|
418
|
+
else:
|
|
419
|
+
_check_dynamic_view_feature_date(session, dynamic_view, timestamp_col, feature_date)
|
|
420
|
+
print(f" [{call_id}] Verified {dynamic_view} serves {timestamp_col} {feature_date}")
|
|
421
|
+
|
|
422
|
+
if isinstance(spine_df, str):
|
|
423
|
+
snowpark_spine_df = session.sql(spine_df)
|
|
424
|
+
print(f"[{call_id}] Created spine from SQL query")
|
|
425
|
+
elif isinstance(spine_df, pd.DataFrame):
|
|
426
|
+
snowpark_spine_df = session.create_dataframe(spine_df)
|
|
427
|
+
print(f"[{call_id}] Created spine from pandas DataFrame")
|
|
428
|
+
else:
|
|
429
|
+
snowpark_spine_df = spine_df
|
|
430
|
+
print(f"[{call_id}] Using provided Snowpark DataFrame")
|
|
431
|
+
|
|
432
|
+
print(f"[{call_id}] Spine contains {snowpark_spine_df.count()} entities")
|
|
433
|
+
|
|
434
|
+
retrieve_kwargs = {
|
|
435
|
+
"spine_df": snowpark_spine_df,
|
|
436
|
+
"features": feature_views_list,
|
|
437
|
+
"include_feature_view_timestamp_col": include_feature_view_timestamp_col,
|
|
438
|
+
}
|
|
439
|
+
if spine_timestamp_col is not None:
|
|
440
|
+
retrieve_kwargs["spine_timestamp_col"] = spine_timestamp_col
|
|
441
|
+
print(f"[{call_id}] Using point-in-time lookup with timestamp column: {spine_timestamp_col}")
|
|
442
|
+
else:
|
|
443
|
+
print(f"[{call_id}] Point-in-time lookup disabled (no spine_timestamp_col)")
|
|
444
|
+
|
|
445
|
+
for fetch_attempt in range(1, max(1, max_fetch_retries) + 1):
|
|
446
|
+
try:
|
|
447
|
+
if fetch_attempt == 1:
|
|
448
|
+
print(f"[{call_id}] Starting retrieve_feature_values (this may take a while)...")
|
|
449
|
+
else:
|
|
450
|
+
print(f"[{call_id}] Retrying retrieve_feature_values "
|
|
451
|
+
f"(attempt {fetch_attempt}/{max_fetch_retries}) after transient error...")
|
|
452
|
+
|
|
453
|
+
snowpark_features = feature_store.retrieve_feature_values(**retrieve_kwargs)
|
|
454
|
+
combined_features = snowpark_features.to_arrow().to_pandas()
|
|
455
|
+
break
|
|
456
|
+
except Exception as fetch_err:
|
|
457
|
+
if _is_transient_fetch_error(fetch_err) and fetch_attempt < max_fetch_retries:
|
|
458
|
+
sleep_for = fetch_retry_backoff_seconds * fetch_attempt
|
|
459
|
+
print(f"⚠️ [{call_id}] Transient fetch error "
|
|
460
|
+
f"({type(fetch_err).__name__}: {fetch_err}). "
|
|
461
|
+
f"Sleeping {sleep_for}s before retry "
|
|
462
|
+
f"{fetch_attempt + 1}/{max_fetch_retries}...")
|
|
463
|
+
time.sleep(sleep_for)
|
|
464
|
+
continue
|
|
465
|
+
raise
|
|
466
|
+
|
|
467
|
+
returned_cols = {c.upper(): c for c in combined_features.columns}
|
|
468
|
+
for col, (dynamic_view, timestamp_col) in feature_date_cols.items():
|
|
469
|
+
if col in returned_cols:
|
|
470
|
+
_check_retrieved_feature_date(combined_features, returned_cols[col], feature_date)
|
|
471
|
+
else:
|
|
472
|
+
_check_dynamic_view_feature_date(session, dynamic_view, timestamp_col, feature_date)
|
|
473
|
+
print(f"[{call_id}] Verified {col} matches feature_date {feature_date}")
|
|
474
|
+
|
|
475
|
+
if spine_timestamp_col is not None and feature_date:
|
|
476
|
+
_warn_spine_before_feature_date(combined_features, spine_timestamp_col, feature_date)
|
|
477
|
+
|
|
478
|
+
combined_features.columns = map(str.lower, combined_features.columns)
|
|
479
|
+
|
|
480
|
+
print(f"✅ [{call_id}] Feature serving completed successfully")
|
|
481
|
+
return combined_features
|
|
482
|
+
|
|
483
|
+
finally:
|
|
484
|
+
lock.release()
|