Skip to content

Commit 9f5b134

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents ce6c04a + b8b7831 commit 9f5b134

File tree

6 files changed

+619
-16
lines changed

6 files changed

+619
-16
lines changed

sdk/python/feast/feature_store.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,23 @@ def write_to_online_store(
18931893
allow_registry_cache=allow_registry_cache,
18941894
transform_on_write=transform_on_write,
18951895
)
1896+
1897+
# Validate that the dataframe has meaningful feature data
1898+
if df is not None:
1899+
if df.empty:
1900+
warnings.warn("Cannot write empty dataframe to online store")
1901+
return # Early return for empty dataframe
1902+
1903+
# Check if feature columns are empty (entity columns may have data but feature columns are empty)
1904+
feature_column_names = [f.name for f in feature_view.features]
1905+
if feature_column_names:
1906+
feature_df = df[feature_column_names]
1907+
if feature_df.empty or feature_df.isnull().all().all():
1908+
warnings.warn(
1909+
"Cannot write dataframe with empty feature columns to online store"
1910+
)
1911+
return # Early return for empty feature columns
1912+
18961913
provider = self._get_provider()
18971914
provider.ingest_df(feature_view, df)
18981915

@@ -1919,6 +1936,23 @@ async def write_to_online_store_async(
19191936
inputs=inputs,
19201937
allow_registry_cache=allow_registry_cache,
19211938
)
1939+
1940+
# Validate that the dataframe has meaningful feature data
1941+
if df is not None:
1942+
if df.empty:
1943+
warnings.warn("Cannot write empty dataframe to online store")
1944+
return # Early return for empty dataframe
1945+
1946+
# Check if feature columns are empty (entity columns may have data but feature columns are empty)
1947+
feature_column_names = [f.name for f in feature_view.features]
1948+
if feature_column_names:
1949+
feature_df = df[feature_column_names]
1950+
if feature_df.empty or feature_df.isnull().all().all():
1951+
warnings.warn(
1952+
"Cannot write dataframe with empty feature columns to online store"
1953+
)
1954+
return # Early return for empty feature columns
1955+
19221956
provider = self._get_provider()
19231957
await provider.ingest_df_async(feature_view, df)
19241958

sdk/python/feast/permissions/auth_model.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
from typing import Literal
1+
# --------------------------------------------------------------------
2+
# Extends OIDC client auth model with an optional `token` field.
3+
# Works on Pydantic v2-only.
4+
#
5+
# Accepted credential sets (exactly **one** of):
6+
# 1 pre-issued `token`
7+
# 2 `client_secret` (client-credentials flow)
8+
# 3 `username` + `password` + `client_secret` (ROPG)
9+
# --------------------------------------------------------------------
10+
from __future__ import annotations
11+
12+
from typing import Literal, Optional
13+
14+
from pydantic import model_validator
215

316
from feast.repo_config import FeastConfigBaseModel
417

@@ -13,9 +26,40 @@ class OidcAuthConfig(AuthConfig):
1326

1427

1528
class OidcClientAuthConfig(OidcAuthConfig):
16-
username: str
17-
password: str
18-
client_secret: str
29+
# any **one** of the four fields below is sufficient
30+
username: Optional[str] = None
31+
password: Optional[str] = None
32+
client_secret: Optional[str] = None
33+
token: Optional[str] = None # pre-issued `token`
34+
35+
@model_validator(mode="after")
36+
def _validate_credentials(cls, values):
37+
"""Enforce exactly one valid credential set."""
38+
d = values.__dict__ if hasattr(values, "__dict__") else values
39+
40+
has_user_pass = bool(d.get("username")) and bool(d.get("password"))
41+
has_secret = bool(d.get("client_secret"))
42+
has_token = bool(d.get("token"))
43+
44+
# 1 static token
45+
if has_token and not (has_user_pass or has_secret):
46+
return values
47+
48+
# 2 client_credentials
49+
if has_secret and not has_user_pass and not has_token:
50+
return values
51+
52+
# 3 ROPG
53+
if has_user_pass and has_secret and not has_token:
54+
return values
55+
56+
raise ValueError(
57+
"Invalid OIDC client auth combination: "
58+
"provide either\n"
59+
" • token\n"
60+
" • client_secret (without username/password)\n"
61+
" • username + password + client_secret"
62+
)
1963

2064

2165
class NoAuthConfig(AuthConfig):

sdk/python/feast/permissions/client/oidc_authentication_client_manager.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,28 @@ def get_token(self):
3030
self.auth_config.auth_discovery_url
3131
).get_token_url()
3232

33-
token_request_body = {
34-
"grant_type": "password",
35-
"client_id": self.auth_config.client_id,
36-
"client_secret": self.auth_config.client_secret,
37-
"username": self.auth_config.username,
38-
"password": self.auth_config.password,
39-
}
33+
# 1) pre-issued JWT supplied in config
34+
if getattr(self.auth_config, "token", None):
35+
return self.auth_config.token
36+
37+
# 2) client_credentials
38+
if self.auth_config.client_secret and not (
39+
self.auth_config.username and self.auth_config.password
40+
):
41+
token_request_body = {
42+
"grant_type": "client_credentials",
43+
"client_id": self.auth_config.client_id,
44+
"client_secret": self.auth_config.client_secret,
45+
}
46+
# 3) ROPG (username + password + client_secret)
47+
else:
48+
token_request_body = {
49+
"grant_type": "password",
50+
"client_id": self.auth_config.client_id,
51+
"client_secret": self.auth_config.client_secret,
52+
"username": self.auth_config.username,
53+
"password": self.auth_config.password,
54+
}
4055
headers = {"Content-Type": "application/x-www-form-urlencoded"}
4156

4257
token_response = requests.post(

sdk/python/feast/repo_config.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,22 @@ def offline_store(self):
308308
def auth_config(self):
309309
if not self._auth:
310310
if isinstance(self.auth, Dict):
311-
is_oidc_client = (
312-
self.auth.get("type") == AuthType.OIDC.value
313-
and "username" in self.auth
314-
and "password" in self.auth
315-
and "client_secret" in self.auth
311+
# treat this auth block as *client-side* OIDC when it matches
312+
# 1) ROPG – username + password + client_secret
313+
# 2) client-credentials – client_secret only
314+
# 3) static token – token
315+
is_oidc_client = self.auth.get("type") == AuthType.OIDC.value and (
316+
(
317+
"username" in self.auth
318+
and "password" in self.auth
319+
and "client_secret" in self.auth
320+
) # 1
321+
or (
322+
"client_secret" in self.auth
323+
and "username" not in self.auth
324+
and "password" not in self.auth
325+
) # 2
326+
or ("token" in self.auth) # 3
316327
)
317328
self._auth = get_auth_config_from_type(
318329
"oidc_client" if is_oidc_client else self.auth.get("type")

0 commit comments

Comments
 (0)