11import base64
22import dataclasses
33import json
4+ from enum import StrEnum
5+ from typing import Final
46
57import sqlalchemy as sql
68
79from . import backend_types_sql as bts
10+ from .errors import (
11+ InvalidAnnotationKeyError ,
12+ MutuallyExclusiveFilterError ,
13+ )
814from .filter_query_models import (
915 AndPredicate ,
1016 FilterQuery ,
1117 KeyExistsPredicate ,
1218 NotPredicate ,
1319 OrPredicate ,
20+ Predicate ,
1421 ValueContainsPredicate ,
22+ ValueEquals ,
1523 ValueEqualsPredicate ,
1624 ValueInPredicate ,
1725)
1826
27+ SYSTEM_KEY_PREFIX : Final [str ] = "system/"
28+
29+
30+ class SystemKey (StrEnum ):
31+ CREATED_BY = f"{ SYSTEM_KEY_PREFIX } pipeline_run.created_by"
32+
33+
34+ SYSTEM_KEY_SUPPORTED_PREDICATES : dict [SystemKey , set [type ]] = {
35+ SystemKey .CREATED_BY : {
36+ KeyExistsPredicate ,
37+ ValueEqualsPredicate ,
38+ ValueInPredicate ,
39+ },
40+ }
41+
1942# ---------------------------------------------------------------------------
2043# PageToken
2144# ---------------------------------------------------------------------------
@@ -39,8 +62,89 @@ def decode(cls, token: str | None) -> "PageToken":
3962 return cls (** json .loads (base64 .b64decode (token )))
4063
4164
42- class MutuallyExclusiveFilterError (ValueError ):
43- pass
65+ # ---------------------------------------------------------------------------
66+ # SystemKey validation and resolution
67+ # ---------------------------------------------------------------------------
68+
69+
70+ def _get_predicate_key (* , predicate : Predicate ) -> str | None :
71+ """Extract the annotation key from a leaf predicate, or None for logical operators."""
72+ match predicate :
73+ case KeyExistsPredicate ():
74+ return predicate .key_exists .key
75+ case ValueEqualsPredicate ():
76+ return predicate .value_equals .key
77+ case ValueContainsPredicate ():
78+ return predicate .value_contains .key
79+ case ValueInPredicate ():
80+ return predicate .value_in .key
81+ case _:
82+ return None
83+
84+
85+ def _check_predicate_allowed (* , predicate : Predicate ) -> None :
86+ """Raise if a system key is used with an unsupported predicate type."""
87+ key = _get_predicate_key (predicate = predicate )
88+ if key is None :
89+ return
90+
91+ try :
92+ system_key = SystemKey (key )
93+ except ValueError :
94+ return
95+
96+ supported = SYSTEM_KEY_SUPPORTED_PREDICATES .get (system_key , set ())
97+ if type (predicate ) not in supported :
98+ raise InvalidAnnotationKeyError (
99+ f"Predicate { type (predicate ).__name__ } is not supported "
100+ f"for system key { system_key !r} . "
101+ f"Supported: { [t .__name__ for t in supported ]} "
102+ )
103+
104+
105+ def _resolve_system_key_value (
106+ * ,
107+ key : str ,
108+ value : str ,
109+ current_user : str | None ,
110+ ) -> str :
111+ """Resolve special placeholder values for system keys."""
112+ if key == SystemKey .CREATED_BY and value == "me" :
113+ return current_user if current_user is not None else ""
114+ return value
115+
116+
117+ def _maybe_resolve_system_values (
118+ * ,
119+ predicate : ValueEqualsPredicate ,
120+ current_user : str | None ,
121+ ) -> ValueEqualsPredicate :
122+ """Resolve special values in a ValueEqualsPredicate."""
123+ key = predicate .value_equals .key
124+ value = predicate .value_equals .value
125+ resolved = _resolve_system_key_value (
126+ key = key ,
127+ value = value ,
128+ current_user = current_user ,
129+ )
130+ if resolved != value :
131+ return ValueEqualsPredicate (value_equals = ValueEquals (key = key , value = resolved ))
132+ return predicate
133+
134+
135+ def _validate_and_resolve_predicate (
136+ * ,
137+ predicate : Predicate ,
138+ current_user : str | None ,
139+ ) -> Predicate :
140+ """Validate system key support, then resolve special values."""
141+ _check_predicate_allowed (predicate = predicate )
142+ if isinstance (predicate , ValueEqualsPredicate ):
143+ return _maybe_resolve_system_values (
144+ predicate = predicate ,
145+ current_user = current_user ,
146+ )
147+ return predicate
44148
45149
46150# ---------------------------------------------------------------------------
@@ -79,7 +183,12 @@ def build_list_filters(
79183
80184 if filter_query_value :
81185 parsed = FilterQuery .model_validate_json (filter_query_value )
82- where_clauses .append (filter_query_to_where_clause (filter_query = parsed ))
186+ where_clauses .append (
187+ filter_query_to_where_clause (
188+ filter_query = parsed ,
189+ current_user = current_user ,
190+ )
191+ )
83192
84193 next_page_token = PageToken (
85194 offset = offset + page_size ,
@@ -93,10 +202,13 @@ def build_list_filters(
93202def filter_query_to_where_clause (
94203 * ,
95204 filter_query : FilterQuery ,
205+ current_user : str | None = None ,
96206) -> sql .ColumnElement :
97207 predicates = filter_query .and_ or filter_query .or_
98208 is_and = filter_query .and_ is not None
99- clauses = [_predicate_to_clause (predicate = p ) for p in predicates ]
209+ clauses = [
210+ _predicate_to_clause (predicate = p , current_user = current_user ) for p in predicates
211+ ]
100212 return sql .and_ (* clauses ) if is_and else sql .or_ (* clauses )
101213
102214
@@ -161,17 +273,35 @@ def _build_filter_where_clauses(
161273
162274def _predicate_to_clause (
163275 * ,
164- predicate ,
276+ predicate : Predicate ,
277+ current_user : str | None = None ,
165278) -> sql .ColumnElement :
279+ predicate = _validate_and_resolve_predicate (
280+ predicate = predicate ,
281+ current_user = current_user ,
282+ )
283+
166284 match predicate :
167285 case AndPredicate ():
168286 return sql .and_ (
169- * [_predicate_to_clause (predicate = p ) for p in predicate .and_ ]
287+ * [
288+ _predicate_to_clause (predicate = p , current_user = current_user )
289+ for p in predicate .and_
290+ ]
170291 )
171292 case OrPredicate ():
172- return sql .or_ (* [_predicate_to_clause (predicate = p ) for p in predicate .or_ ])
293+ return sql .or_ (
294+ * [
295+ _predicate_to_clause (predicate = p , current_user = current_user )
296+ for p in predicate .or_
297+ ]
298+ )
173299 case NotPredicate ():
174- return sql .not_ (_predicate_to_clause (predicate = predicate .not_ ))
300+ return sql .not_ (
301+ _predicate_to_clause (
302+ predicate = predicate .not_ , current_user = current_user
303+ )
304+ )
175305 case KeyExistsPredicate ():
176306 return _key_exists_to_clause (predicate = predicate )
177307 case ValueEqualsPredicate ():
0 commit comments