gooddata-eval 1.71.1.dev2__py3-none-any.whl → 1.71.1.dev3__py3-none-any.whl

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.
@@ -63,29 +63,46 @@ def uri_to_display_name(uri: str) -> str:
63
63
 
64
64
 
65
65
  def validate_cross_references(viz: CreatedVisualization) -> tuple[bool, list[str]]:
66
- """Validate ranking-filter `using`/`attribute` resolve to correct URI prefixes."""
66
+ """Validate ranking-filter `using`/`attribute` resolve to correct URI prefixes.
67
+
68
+ Always returns `(ok, errors)` — a malformed filter produces an error entry, never an
69
+ exception. Anything unusable (None, empty, non-string) used to reach `.startswith()`
70
+ or `dict.get()` and blow up with AttributeError/TypeError mid-evaluation.
71
+
72
+ `using` is required by the AAC schema, `attribute` is optional (see
73
+ `_normalize_ranking_filter`), so an absent/None/empty `attribute` is accepted silently.
74
+ """
67
75
  errors: list[str] = []
68
76
  fields = viz.query.fields
69
77
  for filter_key, filter_dict in viz.query.filter_by.items():
70
78
  if filter_dict.get("type") != "ranking_filter":
71
79
  continue
72
- using_val = filter_dict.get("using", "")
73
- using_uri = _resolve_alias_to_uri(using_val, fields)
74
- field_def = fields.get(using_val)
75
- is_adhoc_agg = isinstance(field_def, AacQueryField) and bool(field_def.aggregation)
76
- if not using_uri.startswith(("metric/", "fact/")) and not is_adhoc_agg:
77
- errors.append(
78
- f"ranking filter '{filter_key}': using='{using_val}' "
79
- f"resolves to '{using_uri}' — expected a metric/ or fact/ URI"
80
- )
81
- if "attribute" in filter_dict:
82
- attr_val = filter_dict["attribute"]
83
- attr_uri = _resolve_alias_to_uri(attr_val, fields)
84
- if not attr_uri.startswith(("label/", "attribute/")):
80
+ using_val = filter_dict.get("using")
81
+ if not isinstance(using_val, str) or not using_val:
82
+ errors.append(f"ranking filter '{filter_key}': using={using_val!r} — a metric/ or fact/ URI is required")
83
+ else:
84
+ using_uri = _resolve_alias_to_uri(using_val, fields)
85
+ field_def = fields.get(using_val)
86
+ is_adhoc_agg = isinstance(field_def, AacQueryField) and bool(field_def.aggregation)
87
+ if not using_uri.startswith(("metric/", "fact/")) and not is_adhoc_agg:
85
88
  errors.append(
86
- f"ranking filter '{filter_key}': attribute='{attr_val}' "
87
- f"resolves to '{attr_uri}' — expected a label/ or attribute/ URI"
89
+ f"ranking filter '{filter_key}': using='{using_val}' "
90
+ f"resolves to '{using_uri}' — expected a metric/ or fact/ URI"
88
91
  )
92
+ attr_val = filter_dict.get("attribute")
93
+ if attr_val is None or attr_val == "":
94
+ continue
95
+ if not isinstance(attr_val, str):
96
+ errors.append(
97
+ f"ranking filter '{filter_key}': attribute={attr_val!r} — expected a label/ or attribute/ URI"
98
+ )
99
+ continue
100
+ attr_uri = _resolve_alias_to_uri(attr_val, fields)
101
+ if not attr_uri.startswith(("label/", "attribute/")):
102
+ errors.append(
103
+ f"ranking filter '{filter_key}': attribute='{attr_val}' "
104
+ f"resolves to '{attr_uri}' — expected a label/ or attribute/ URI"
105
+ )
89
106
  return len(errors) == 0, errors
90
107
 
91
108
 
@@ -99,11 +116,43 @@ def _normalize_date_filter(filter_dict: dict, _fields: dict) -> dict:
99
116
  }
100
117
 
101
118
 
102
- def _normalize_ranking_filter(filter_dict: dict, fields: dict[str, AacQueryField | str]) -> dict:
119
+ def _sole_dimension_uri(viz: CreatedVisualization) -> str | None:
120
+ """URI of the visualization's only dimension, or None when it has zero or several."""
121
+ dim_uris = get_dimension_uri_set(viz)
122
+ return next(iter(dim_uris)) if len(dim_uris) == 1 else None
123
+
124
+
125
+ def _normalize_ranking_filter(
126
+ filter_dict: dict,
127
+ fields: dict[str, AacQueryField | str],
128
+ sole_dim_uri: str | None = None,
129
+ ) -> dict:
130
+ """Canonicalize a ranking filter so equivalent filters compare equal.
131
+
132
+ `attribute` is optional in the AAC schema (gen-ai models it as `NotRequired[str]` /
133
+ `str | None`), and when it is omitted AFM ranks over every dimension of the result. For a
134
+ single-dimension visualization that is exactly "rank by that one dimension", so an omitted
135
+ attribute is filled in with `sole_dim_uri` instead of comparing as an empty string — the
136
+ agent and the dataset may legitimately express the same filter either way.
137
+
138
+ The substitution is deliberately gated on there being exactly ONE dimension: with two or
139
+ more, omitting `attribute` ranks over the dimension *tuple*, which is a different filter,
140
+ so those stay strict. Callers pass the sole dimension of the visualization the filter
141
+ belongs to, which makes the comparison symmetric — it does not matter which side omitted it.
142
+
143
+ Missing, None and "" are all treated as "not specified"; so is a non-string, which
144
+ `validate_cross_references` reports separately rather than crashing the comparison.
145
+ """
146
+ attr_val = filter_dict.get("attribute")
147
+ if not isinstance(attr_val, str) or not attr_val:
148
+ dim_uri = sole_dim_uri or ""
149
+ else:
150
+ dim_uri = _resolve_alias_to_uri(attr_val, fields)
151
+ using_val = filter_dict.get("using")
103
152
  entry: dict = {
104
153
  "type": "ranking_filter",
105
- "metric_uri": _resolve_alias_to_uri(filter_dict.get("using", ""), fields),
106
- "dim_uri": _resolve_alias_to_uri(filter_dict.get("attribute", ""), fields),
154
+ "metric_uri": _resolve_alias_to_uri(using_val, fields) if isinstance(using_val, str) else "",
155
+ "dim_uri": dim_uri,
107
156
  }
108
157
  if "top" in filter_dict:
109
158
  entry["top"] = filter_dict["top"]
@@ -127,12 +176,13 @@ def _split_and_normalize_filters(viz: CreatedVisualization) -> tuple[set[str], s
127
176
  ranking_set: set[str] = set()
128
177
  attr_set: set[str] = set()
129
178
  fields = viz.query.fields
179
+ sole_dim_uri = _sole_dimension_uri(viz)
130
180
  for filter_dict in viz.query.filter_by.values():
131
181
  ft = filter_dict.get("type")
132
182
  if ft == "date_filter":
133
183
  date_set.add(json.dumps(_normalize_date_filter(filter_dict, fields), sort_keys=True))
134
184
  elif ft == "ranking_filter":
135
- ranking_set.add(json.dumps(_normalize_ranking_filter(filter_dict, fields), sort_keys=True))
185
+ ranking_set.add(json.dumps(_normalize_ranking_filter(filter_dict, fields, sole_dim_uri), sort_keys=True))
136
186
  elif ft == "attribute_filter":
137
187
  attr_set.add(json.dumps(_normalize_attribute_filter(filter_dict, fields), sort_keys=True))
138
188
  return date_set, ranking_set, attr_set
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gooddata-eval
3
- Version: 1.71.1.dev2
3
+ Version: 1.71.1.dev3
4
4
  Summary: Evaluate the GoodData AI agent against your own questions and models.
5
5
  Project-URL: Source, https://github.com/gooddata/gooddata-python-sdk
6
6
  Author-email: GoodData <support@gooddata.com>
@@ -17,7 +17,7 @@ Classifier: Topic :: Scientific/Engineering
17
17
  Classifier: Topic :: Software Development
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: gooddata-sdk~=1.71.1.dev2
20
+ Requires-Dist: gooddata-sdk~=1.71.1.dev3
21
21
  Requires-Dist: httpx<1.0,>=0.27
22
22
  Requires-Dist: orjson<4.0.0,>=3.9.15
23
23
  Requires-Dist: pydantic<3.0,>=2.6
@@ -8,7 +8,7 @@ gooddata_eval/core/config.py,sha256=mFtztP727uAhZ6b7twM5pFvzyazzMGVdgTYMvkqZT2Y,
8
8
  gooddata_eval/core/connection.py,sha256=mh-Do5-Ti0-DXXikfCIH10eTTUbKuZUAS_sLoPNDJ5I,1229
9
9
  gooddata_eval/core/models.py,sha256=FdGGZUSp1Qcs98Ib5U3OhGplBlUZMimlIdAqGLTjFDU,4473
10
10
  gooddata_eval/core/runner.py,sha256=sxE7kWDG6ttkSf5kUkZEJJ-B3DqWHXQOFBMEjMr4-D4,8520
11
- gooddata_eval/core/scoring.py,sha256=7ku48WEXtZl8EhowKikMiBmKfH7BDoZxHVvEOY8h3Rk,6007
11
+ gooddata_eval/core/scoring.py,sha256=NbZnQeDuH1DF0z7RwxoeuQaTN_b4-85uu6Vm1QTjekI,8544
12
12
  gooddata_eval/core/workspace.py,sha256=S8exiy5gpDtcRtT8-i2fIRALF34Pbo0J46xm8X_ldus,11846
13
13
  gooddata_eval/core/agentic/__init__.py,sha256=nPjJ9sbnJVvsAlo9QMQlnPFgLNTZRzy9-0IKlnRcU8Q,2671
14
14
  gooddata_eval/core/agentic/_catalog.py,sha256=eCaeyvLF-FCN0J3UnXSHaKtufU6HvrM2mxnmgyXsY1U,2085
@@ -44,8 +44,8 @@ gooddata_eval/core/reporting/console.py,sha256=kCHt4sCSaSRh0ZWqaSEJOl6_Jgb6uiouS
44
44
  gooddata_eval/core/reporting/json_report.py,sha256=sKPG9-uxzDsf6EJyBXoaHVIfN1g-E2rZ8fA0wtyBYb4,2943
45
45
  gooddata_eval/core/summary/__init__.py,sha256=U54lANKm34yjqm7dZL9KkGouPbwAaQWC9wiAmb5B5g4,32
46
46
  gooddata_eval/core/summary/http_client.py,sha256=dPArJ6zoCu1W9xmYXFA1WMDNsSOkhn3gaCpYgCUp3gk,2174
47
- gooddata_eval-1.71.1.dev2.dist-info/METADATA,sha256=tYIsUFpURQNMt4xXSE2G77zigVMdvzbvZzWw_wfOxwA,9995
48
- gooddata_eval-1.71.1.dev2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
49
- gooddata_eval-1.71.1.dev2.dist-info/entry_points.txt,sha256=28nFp5Viknx4haPYgzK9QHlwPFfC6rPF6RmjC2ht8EI,56
50
- gooddata_eval-1.71.1.dev2.dist-info/licenses/LICENSE.txt,sha256=LVfVlC9maU3K9lgMwdZnClQQsOIbOekdcdVKr9qzJtk,256836
51
- gooddata_eval-1.71.1.dev2.dist-info/RECORD,,
47
+ gooddata_eval-1.71.1.dev3.dist-info/METADATA,sha256=ylGcgPJSeYQvXqCFnhfKf46z17jI_UZ3hKcoksHvjA0,9995
48
+ gooddata_eval-1.71.1.dev3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
49
+ gooddata_eval-1.71.1.dev3.dist-info/entry_points.txt,sha256=28nFp5Viknx4haPYgzK9QHlwPFfC6rPF6RmjC2ht8EI,56
50
+ gooddata_eval-1.71.1.dev3.dist-info/licenses/LICENSE.txt,sha256=LVfVlC9maU3K9lgMwdZnClQQsOIbOekdcdVKr9qzJtk,256836
51
+ gooddata_eval-1.71.1.dev3.dist-info/RECORD,,