gooddata-eval 1.78.1.dev4__tar.gz → 1.79.1__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.
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/PKG-INFO +2 -2
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/pyproject.toml +2 -2
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/dashboard_skill.py +300 -41
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_dashboard_skill.py +366 -1
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/.gitignore +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/AGENTS.md +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/CLAUDE.md +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/LICENSE.txt +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/Makefile +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/README.md +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/scripts/calibrate_takeaway_judge.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/scripts/verify_guardrail_refusal_criteria.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/_version.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/cli/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/cli/agentic_runner.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/cli/main.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/_output.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/_retry.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/_catalog.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/_conversation_context.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/_failed_runs.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/_gate.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/_langfuse.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/_trace_linker.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/alert_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/anomaly_detection.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/conversation.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/document_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/general_question.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/guardrail.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/kda_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/metric_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/report_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/search_tool.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/visualization.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/what_if.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/chat/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/chat/render.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/chat/sse_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/config.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/connection.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/dataset/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/dataset/from_insights.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/dataset/langfuse_source.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/dataset/local.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/_deep_subset.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/_guardrail_criteria.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/_llm_judge.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/_maql.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/_rubric.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/_text_utils.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/alert_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/base.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/general_question.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/guardrail.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/metric_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/search_tool.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/summary.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/takeaway.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/evaluators/visualization.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/granularity.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/interaction.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/_env.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/experiment.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/item_scope.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/observations.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/otlp.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/langfuse/sink.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/models.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/number_format.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/reporting/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/reporting/console.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/reporting/html_report.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/reporting/json_report.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/reporting/report_template.html +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/runner.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/scoring.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/summary/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/summary/http_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/takeaway/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/takeaway/evaluate.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/takeaway/http_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/timing.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/workspace.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/__init__.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/_fake_langfuse.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/conftest.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/fixtures/sample_dataset/metric_skill_create.json +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/fixtures/sample_dataset/visualization_revenue.json +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/fixtures/sse_visualization_stream.txt +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/fixtures/takeaway_calibration.json +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_alert_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_anomaly_detection.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_conversation.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_document_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_gate.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_general_question.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_guardrail.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_join_trace.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_kda_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_langfuse_trace.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_metric_skill.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_observe_experiment.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_run_context.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_runner.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_search_tool.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_visualization.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_agentic_what_if.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_alert_skill_evaluator.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_chat_render.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_cli.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_connection.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_deep_subset.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_fake_langfuse.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_from_insights.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_guardrail_criteria.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_html_report.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_interaction.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_e2e_fake_server.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_env.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_experiment.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_item_scope.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_observations.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_otlp.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_sink.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_langfuse_source.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_llm_judge.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_local_loader.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_maql_normalize.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_metric_skill_evaluator.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_models.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_number_format.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_reporting.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_runner.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_scoring.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_search_tool_evaluator.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_sse_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_summary_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_summary_evaluator.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_takeaway_client.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_takeaway_evaluate.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_takeaway_evaluator.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_text_evaluators.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_timing.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_trace_linker.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_visualization_evaluator.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tests/test_workspace.py +0 -0
- {gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gooddata-eval
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.79.1
|
|
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.
|
|
20
|
+
Requires-Dist: gooddata-sdk~=1.79.1
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
22
|
Requires-Dist: jsonpatch<2.0,>=1.33
|
|
23
23
|
Requires-Dist: orjson<4.0.0,>=3.9.15
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# (C) 2026 GoodData Corporation
|
|
2
2
|
[project]
|
|
3
3
|
name = "gooddata-eval"
|
|
4
|
-
version = "1.
|
|
4
|
+
version = "1.79.1"
|
|
5
5
|
description = "Evaluate the GoodData AI agent against your own questions and models."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
keywords = ["gooddata", "ai", "evaluation", "llm", "analytics", "cli"]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"gooddata-sdk~=1.
|
|
14
|
+
"gooddata-sdk~=1.79.1",
|
|
15
15
|
"httpx>=0.27,<1.0",
|
|
16
16
|
"jsonpatch>=1.33,<2.0",
|
|
17
17
|
"orjson>=3.9.15,<4.0.0",
|
{gooddata_eval-1.78.1.dev4 → gooddata_eval-1.79.1}/src/gooddata_eval/core/agentic/dashboard_skill.py
RENAMED
|
@@ -313,20 +313,33 @@ def _grouped_filter_entries(dashboard: dict) -> list[tuple[str, dict, str | None
|
|
|
313
313
|
if tabs
|
|
314
314
|
else [("", dashboard.get("filters") or {})]
|
|
315
315
|
)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
for
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
316
|
+
return [
|
|
317
|
+
(where or f"filter {entry_key!r}", value, group)
|
|
318
|
+
for where, filters in maps
|
|
319
|
+
for entry_key, _key, value, group in _members_of(filters)
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _members_of(filters: dict) -> list[tuple[str, str, dict, str | None]]:
|
|
324
|
+
"""``(entry key, filter key, filter, group title)`` for every filter of one filter map.
|
|
325
|
+
|
|
326
|
+
A group's members are read in its place, each under its own key and with the group's key as
|
|
327
|
+
the entry key; a filter outside a group is its own entry and has no group title.
|
|
328
|
+
"""
|
|
329
|
+
members: list[tuple[str, str, dict, str | None]] = []
|
|
330
|
+
for entry_key, value in filters.items():
|
|
331
|
+
if not isinstance(value, dict):
|
|
332
|
+
continue
|
|
333
|
+
if value.get("type") != "filter_group":
|
|
334
|
+
members.append((entry_key, entry_key, value, None))
|
|
335
|
+
continue
|
|
336
|
+
group = str(value.get("title") or "")
|
|
337
|
+
members.extend(
|
|
338
|
+
(entry_key, key, member, group)
|
|
339
|
+
for key, member in (value.get("filters") or {}).items()
|
|
340
|
+
if isinstance(member, dict)
|
|
341
|
+
)
|
|
342
|
+
return members
|
|
330
343
|
|
|
331
344
|
|
|
332
345
|
def _filters_of_type(dashboard: dict, filter_type: str) -> list[tuple[str, dict]]:
|
|
@@ -549,6 +562,11 @@ def _matching_widgets(widgets: list[dict], new_ids: set[str], expected: dict) ->
|
|
|
549
562
|
return [w for w in widgets if w.get("visualization") in new_ids and _norm(str(w.get("title") or "")) == title]
|
|
550
563
|
|
|
551
564
|
|
|
565
|
+
def _chart_entry(chart: str | dict) -> dict:
|
|
566
|
+
"""A chart as ``tabs`` and ``layout`` name it -- an id, or ``{"title": ...}`` -- as a ``_matching_widgets`` entry."""
|
|
567
|
+
return {"id": chart} if isinstance(chart, str) else {"id": None, **chart}
|
|
568
|
+
|
|
569
|
+
|
|
552
570
|
def _has_date_bindings(expected_output: dict) -> bool:
|
|
553
571
|
"""Whether any expected chart states the date dataset its widget must follow.
|
|
554
572
|
|
|
@@ -665,10 +683,11 @@ def _check_tabs(dashboard: dict, new_ids: set[str], expected: list[dict]) -> lis
|
|
|
665
683
|
if "title" in exp and _norm(str(tab.get("title") or "")) != _norm(str(exp["title"] or "")):
|
|
666
684
|
failures.append(f"tab {index} is titled {tab.get('title')!r}, expected {exp['title']!r}")
|
|
667
685
|
tab_widgets = [w for section in tab.get("sections") or [] for w in section.get("widgets") or []]
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
686
|
+
failures.extend(
|
|
687
|
+
f"tab {index} ({tab.get('title')!r}) does not hold chart {chart!r}"
|
|
688
|
+
for chart in exp.get("visualizations") or []
|
|
689
|
+
if not _matching_widgets(tab_widgets, new_ids, _chart_entry(chart))
|
|
690
|
+
)
|
|
672
691
|
return failures
|
|
673
692
|
|
|
674
693
|
|
|
@@ -732,27 +751,10 @@ def _check_tab_filters(dashboard: dict, expected: dict[str, dict]) -> list[str]:
|
|
|
732
751
|
Exact on purpose: GDAI-2540 is a filter that turns up on a tab it never belonged to, which a
|
|
733
752
|
check of only the expected entries would pass. Keyed by the saved dashboard's local filter
|
|
734
753
|
id rather than by label, so two filters on the same label stay distinguishable.
|
|
754
|
+
|
|
755
|
+
It is ``filter_bar`` with every filter known by key, so the two read filter groups the same way.
|
|
735
756
|
"""
|
|
736
|
-
|
|
737
|
-
failures: list[str] = []
|
|
738
|
-
for tab_id, expected_filters in expected.items():
|
|
739
|
-
tab = tabs.get(tab_id)
|
|
740
|
-
if tab is None:
|
|
741
|
-
failures.append(f"the dashboard has no tab {tab_id!r}")
|
|
742
|
-
continue
|
|
743
|
-
actual = {k: v for k, v in (tab.get("filters") or {}).items() if isinstance(v, dict)}
|
|
744
|
-
failures.extend(
|
|
745
|
-
f"tab {tab_id!r} carries filter {filter_id!r}, which it should not"
|
|
746
|
-
for filter_id in sorted(actual.keys() - expected_filters.keys())
|
|
747
|
-
)
|
|
748
|
-
failures.extend(
|
|
749
|
-
f"tab {tab_id!r} lost filter {filter_id!r}" for filter_id in sorted(expected_filters.keys() - actual.keys())
|
|
750
|
-
)
|
|
751
|
-
for filter_id in sorted(expected_filters.keys() & actual.keys()):
|
|
752
|
-
failures.extend(
|
|
753
|
-
_filter_mismatch(f"tab {tab_id!r} filter {filter_id!r}", actual[filter_id], expected_filters[filter_id])
|
|
754
|
-
)
|
|
755
|
-
return failures
|
|
757
|
+
return _check_filter_bar(dashboard, [{"tab": tab_id, "filters": filters} for tab_id, filters in expected.items()])
|
|
756
758
|
|
|
757
759
|
|
|
758
760
|
def _filters_by_id(dashboard: dict) -> dict[str, list[tuple[str, dict]]]:
|
|
@@ -819,6 +821,167 @@ def _check_preserved_widgets(widgets: list[dict], expected: list[dict]) -> list[
|
|
|
819
821
|
return failures
|
|
820
822
|
|
|
821
823
|
|
|
824
|
+
def _named_tab(dashboard: dict, tab_id: str | None) -> tuple[dict | None, str]:
|
|
825
|
+
"""The tab an entry of ``layout`` or ``filter_bar`` names, and how to refer to it in a failure.
|
|
826
|
+
|
|
827
|
+
An entry without ``tab`` is for a dashboard with one tab only, root sections included: on a
|
|
828
|
+
dashboard with several it would be a guess which one the case meant, so it is a failure.
|
|
829
|
+
"""
|
|
830
|
+
tabs = _tabs_of(dashboard)
|
|
831
|
+
if tab_id is None:
|
|
832
|
+
if len(tabs) != 1:
|
|
833
|
+
return None, f"the dashboard has {len(tabs)} tabs, so the expectation must name one"
|
|
834
|
+
return tabs[0], "the dashboard"
|
|
835
|
+
tab = next((t for t in tabs if t.get("id") == tab_id), None)
|
|
836
|
+
return tab, f"tab {tab_id!r}" if tab is not None else f"the dashboard has no tab {tab_id!r}"
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _check_layout(dashboard: dict, new_ids: set[str], expected: list[dict]) -> list[str]:
|
|
840
|
+
"""Each named tab must hold exactly the expected sections, in order, each with exactly its charts in order.
|
|
841
|
+
|
|
842
|
+
Exact on purpose. A move that leaves the widget where it was, places a second copy, or puts
|
|
843
|
+
it in the wrong section keeps every chart "on the dashboard", and so does a removal that
|
|
844
|
+
never happened; only the whole arrangement tells them apart. A section's ``title`` is
|
|
845
|
+
compared when the entry states it, ``null`` meaning untitled. Its ``visualizations`` are
|
|
846
|
+
always stated, ``[]`` for a section without charts. A chart is an id, or
|
|
847
|
+
``{"title": ...}`` for one the agent authored. Widgets that show no visualization are not
|
|
848
|
+
charts and are left out on both sides.
|
|
849
|
+
"""
|
|
850
|
+
failures: list[str] = []
|
|
851
|
+
for entry in expected:
|
|
852
|
+
tab, where = _named_tab(dashboard, entry.get("tab"))
|
|
853
|
+
if tab is None:
|
|
854
|
+
failures.append(where)
|
|
855
|
+
continue
|
|
856
|
+
sections = tab.get("sections") or []
|
|
857
|
+
wanted = entry["sections"]
|
|
858
|
+
if len(sections) != len(wanted):
|
|
859
|
+
titles = ", ".join(repr(s.get("title")) for s in sections)
|
|
860
|
+
failures.append(f"{where} has {len(sections)} section(s) ({titles}), expected {len(wanted)}")
|
|
861
|
+
continue
|
|
862
|
+
for index, (section, want) in enumerate(zip(sections, wanted, strict=True)):
|
|
863
|
+
if "title" in want and _norm(str(section.get("title") or "")) != _norm(str(want["title"] or "")):
|
|
864
|
+
failures.append(
|
|
865
|
+
f"{where} section {index} is titled {section.get('title')!r}, expected {want['title']!r}"
|
|
866
|
+
)
|
|
867
|
+
charts = [w for w in section.get("widgets") or [] if w.get("visualization")]
|
|
868
|
+
charts_wanted = want["visualizations"]
|
|
869
|
+
if len(charts) != len(charts_wanted) or not all(
|
|
870
|
+
_matching_widgets([w], new_ids, _chart_entry(c)) for w, c in zip(charts, charts_wanted, strict=True)
|
|
871
|
+
):
|
|
872
|
+
shown = [w.get("visualization") for w in charts]
|
|
873
|
+
failures.append(f"{where} section {index} holds {shown}, expected {charts_wanted}")
|
|
874
|
+
return failures
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _check_filter_bar(dashboard: dict, expected: list[dict]) -> list[str]:
|
|
878
|
+
"""Each named tab must carry exactly the expected filters: the kept ones by key, the new ones by content.
|
|
879
|
+
|
|
880
|
+
``filters`` names a filter the case knows the key of, the saved one or one the change keeps.
|
|
881
|
+
``added`` holds a filter whose key the agent mints at run time -- a new one, or one it
|
|
882
|
+
replaced by removing and re-adding it -- each matched to a different filter that ``filters``
|
|
883
|
+
did not claim. Anything left over fails: a filter that should have been removed, a duplicate,
|
|
884
|
+
or one copied in from elsewhere. Unlike ``tab_filters``, it reads a dashboard without tabs too.
|
|
885
|
+
|
|
886
|
+
A grouped filter is read by its own key. Its entry names the group's title as ``group``, and
|
|
887
|
+
an entry without one must sit outside every group, so a filter moved into or out of a group
|
|
888
|
+
fails. A group none of the expected filters sits in fails like a leftover filter -- an empty
|
|
889
|
+
one, or one whose members should have been ungrouped -- and so does a key the tab carries twice.
|
|
890
|
+
"""
|
|
891
|
+
failures: list[str] = []
|
|
892
|
+
for entry in expected:
|
|
893
|
+
tab, where = _named_tab(dashboard, entry.get("tab"))
|
|
894
|
+
if tab is None:
|
|
895
|
+
failures.append(where)
|
|
896
|
+
continue
|
|
897
|
+
filters = tab.get("filters") or {}
|
|
898
|
+
groups = {
|
|
899
|
+
key: str(value.get("title") or "")
|
|
900
|
+
for key, value in filters.items()
|
|
901
|
+
if isinstance(value, dict) and value.get("type") == "filter_group"
|
|
902
|
+
}
|
|
903
|
+
# Each filter by its own key, with the key of the group it sits in.
|
|
904
|
+
actual: dict[str, tuple[dict, str | None]] = {}
|
|
905
|
+
for entry_key, key, value, group in _members_of(filters):
|
|
906
|
+
if key in actual:
|
|
907
|
+
failures.append(f"{where} carries filter {key!r} more than once")
|
|
908
|
+
continue
|
|
909
|
+
actual[key] = (value, None if group is None else entry_key)
|
|
910
|
+
claimed: set[str | None] = set()
|
|
911
|
+
for filter_id, want in (entry.get("filters") or {}).items():
|
|
912
|
+
got = actual.pop(filter_id, None)
|
|
913
|
+
if got is None:
|
|
914
|
+
failures.append(f"{where} lost filter {filter_id!r}")
|
|
915
|
+
continue
|
|
916
|
+
value, group_key = got
|
|
917
|
+
failures.extend(_filter_mismatch(f"{where} filter {filter_id!r}", value, _without_group(want)))
|
|
918
|
+
if _in_wanted_group(groups, group_key, want):
|
|
919
|
+
claimed.add(group_key)
|
|
920
|
+
else:
|
|
921
|
+
sits = "no group" if group_key is None else f"group {groups[group_key]!r}"
|
|
922
|
+
wanted = f"group {want['group']!r}" if "group" in want else "no group"
|
|
923
|
+
failures.append(f"{where} filter {filter_id!r} sits in {sits}, expected {wanted}")
|
|
924
|
+
added = entry.get("added") or []
|
|
925
|
+
keys = list(actual)
|
|
926
|
+
fits = [
|
|
927
|
+
[
|
|
928
|
+
not _filter_mismatch("", actual[key][0], _without_group(want))
|
|
929
|
+
and _in_wanted_group(groups, actual[key][1], want)
|
|
930
|
+
for key in keys
|
|
931
|
+
]
|
|
932
|
+
for want in added
|
|
933
|
+
]
|
|
934
|
+
for want, index in zip(added, _one_to_one(fits), strict=True):
|
|
935
|
+
if index is None:
|
|
936
|
+
failures.append(f"{where} carries no new filter matching {want!r}")
|
|
937
|
+
continue
|
|
938
|
+
claimed.add(actual.pop(keys[index])[1])
|
|
939
|
+
failures.extend(f"{where} carries filter {key!r}, which it should not" for key in sorted(actual))
|
|
940
|
+
failures.extend(
|
|
941
|
+
f"{where} carries filter group {key!r} ({title!r}), which holds none of the expected filters"
|
|
942
|
+
for key, title in sorted(groups.items())
|
|
943
|
+
if key not in claimed
|
|
944
|
+
)
|
|
945
|
+
return failures
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _without_group(expected: dict) -> dict:
|
|
949
|
+
return {k: v for k, v in expected.items() if k != "group"}
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _in_wanted_group(groups: dict[str, str], group_key: str | None, expected: dict) -> bool:
|
|
953
|
+
"""Whether a filter in the group keyed ``group_key`` (``None`` for none) sits where ``expected`` puts it."""
|
|
954
|
+
if group_key is None or "group" not in expected:
|
|
955
|
+
return group_key is None and "group" not in expected
|
|
956
|
+
return _norm(groups[group_key]) == _norm(expected["group"])
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _one_to_one(fits: list[list[bool]]) -> list[int | None]:
|
|
960
|
+
"""For each row, a column it fits, no column taken twice, with as many rows matched as there can be.
|
|
961
|
+
|
|
962
|
+
First fit is not enough: an expectation that names fewer keys can take the filter a stricter
|
|
963
|
+
one needed and fail a correct dashboard on the order the entries are listed in. Each row
|
|
964
|
+
that finds every fitting column taken tries to move that column's owner to another one.
|
|
965
|
+
"""
|
|
966
|
+
owner: dict[int, int] = {}
|
|
967
|
+
|
|
968
|
+
def claim(row: int, seen: set[int]) -> bool:
|
|
969
|
+
for column, fit in enumerate(fits[row]):
|
|
970
|
+
if fit and column not in seen:
|
|
971
|
+
seen.add(column)
|
|
972
|
+
if column not in owner or claim(owner[column], seen):
|
|
973
|
+
owner[column] = row
|
|
974
|
+
return True
|
|
975
|
+
return False
|
|
976
|
+
|
|
977
|
+
for row in range(len(fits)):
|
|
978
|
+
claim(row, set())
|
|
979
|
+
matched: list[int | None] = [None] * len(fits)
|
|
980
|
+
for column, row in owner.items():
|
|
981
|
+
matched[row] = column
|
|
982
|
+
return matched
|
|
983
|
+
|
|
984
|
+
|
|
822
985
|
def _date_range_text(date_range: dict | None) -> str:
|
|
823
986
|
"""Render a date range the way a user would say it, for the simulated reply.
|
|
824
987
|
|
|
@@ -986,7 +1149,7 @@ def _validate_expectation(expected_output: dict) -> None:
|
|
|
986
1149
|
|
|
987
1150
|
|
|
988
1151
|
def _validate_layout_expectations(expected_output: dict) -> None:
|
|
989
|
-
"""Shape checks for the opt-in keys on charts, tabs, per-tab filters and
|
|
1152
|
+
"""Shape checks for the opt-in keys on charts, tabs, per-tab filters, preserved widgets, layout and filter bar.
|
|
990
1153
|
|
|
991
1154
|
Raises:
|
|
992
1155
|
ValueError: one of them is unusable. Loud for the same reason as the rest of the
|
|
@@ -1023,8 +1186,7 @@ def _validate_layout_expectations(expected_output: dict) -> None:
|
|
|
1023
1186
|
):
|
|
1024
1187
|
raise ValueError(f"tab_filters[{tab_id!r}] must map filter ids to typed filters, got {filters!r}")
|
|
1025
1188
|
for filter_id, entry in filters.items():
|
|
1026
|
-
|
|
1027
|
-
_validate_tab_attribute_filter(f"tab_filters[{tab_id!r}][{filter_id!r}]", entry)
|
|
1189
|
+
_validate_bar_filter(f"tab_filters[{tab_id!r}][{filter_id!r}]", entry)
|
|
1028
1190
|
|
|
1029
1191
|
if "date_filters" in expected_output:
|
|
1030
1192
|
date_filters = expected_output["date_filters"]
|
|
@@ -1057,6 +1219,87 @@ def _validate_layout_expectations(expected_output: dict) -> None:
|
|
|
1057
1219
|
if not isinstance(widget, dict) or not widget.get("visualization") or len(widget) < 2:
|
|
1058
1220
|
raise ValueError(f"a preserved widget needs its visualization and a key to guard, got {widget!r}")
|
|
1059
1221
|
|
|
1222
|
+
if "layout" in expected_output:
|
|
1223
|
+
for entry in _tab_entries(expected_output, "layout", {"tab", "sections"}):
|
|
1224
|
+
sections = entry.get("sections")
|
|
1225
|
+
if not isinstance(sections, list):
|
|
1226
|
+
raise ValueError(f"a layout entry needs its sections as a list, got {entry!r}")
|
|
1227
|
+
for section in sections:
|
|
1228
|
+
if not isinstance(section, dict) or section.keys() - {"title", "visualizations"}:
|
|
1229
|
+
raise ValueError(f"a layout section takes only title and visualizations, got {section!r}")
|
|
1230
|
+
if section.get("title") is not None and not isinstance(section["title"], str):
|
|
1231
|
+
raise ValueError(f"a layout section's title must be a string or null, got {section!r}")
|
|
1232
|
+
if "visualizations" not in section:
|
|
1233
|
+
# Missing would read as "no charts" here but as "any charts" in `tabs`.
|
|
1234
|
+
raise ValueError(f"a layout section must list its visualizations, [] for none, got {section!r}")
|
|
1235
|
+
charts = section["visualizations"]
|
|
1236
|
+
if not isinstance(charts, list) or not all(
|
|
1237
|
+
(isinstance(c, str) and c)
|
|
1238
|
+
or (isinstance(c, dict) and c.keys() == {"title"} and isinstance(c["title"], str) and c["title"])
|
|
1239
|
+
for c in charts
|
|
1240
|
+
):
|
|
1241
|
+
raise ValueError(
|
|
1242
|
+
f"a layout section's visualizations must be chart ids or {{'title': ...}}, got {section!r}"
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
if "filter_bar" in expected_output:
|
|
1246
|
+
for entry in _tab_entries(expected_output, "filter_bar", {"tab", "filters", "added"}):
|
|
1247
|
+
where = f"filter_bar[{entry.get('tab')!r}]"
|
|
1248
|
+
kept = entry.get("filters", {})
|
|
1249
|
+
added = entry.get("added", [])
|
|
1250
|
+
if not isinstance(kept, dict) or not isinstance(added, list):
|
|
1251
|
+
raise ValueError(f"{where} needs filters as a map of keys and added as a list, got {entry!r}")
|
|
1252
|
+
named = [(f"{where}[{key!r}]", f) for key, f in kept.items()]
|
|
1253
|
+
named += [(f"{where} added[{index}]", f) for index, f in enumerate(added)]
|
|
1254
|
+
for label, body in named:
|
|
1255
|
+
_validate_bar_filter(label, body)
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _validate_bar_filter(label: str, body: object) -> None:
|
|
1259
|
+
"""A filter ``tab_filters`` or ``filter_bar`` expects: typed, in the AAC shape, and never a group.
|
|
1260
|
+
|
|
1261
|
+
A group is not a filter there: both checks read its members by their own keys, so an
|
|
1262
|
+
expected ``filter_group`` could never match. Its members say which group they sit in.
|
|
1263
|
+
|
|
1264
|
+
Raises:
|
|
1265
|
+
ValueError: the filter is unusable.
|
|
1266
|
+
"""
|
|
1267
|
+
if not isinstance(body, dict) or not body.get("type"):
|
|
1268
|
+
raise ValueError(f"{label} must be a typed filter, got {body!r}")
|
|
1269
|
+
if body["type"] == "filter_group":
|
|
1270
|
+
raise ValueError(f"{label} is a filter_group; list its members instead, each with group set to its title")
|
|
1271
|
+
if "group" in body and not (isinstance(body["group"], str) and body["group"]):
|
|
1272
|
+
raise ValueError(f"{label} group must be the title of the filter group, got {body['group']!r}")
|
|
1273
|
+
if body["type"] == "attribute_filter":
|
|
1274
|
+
_validate_tab_attribute_filter(label, body)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def _tab_entries(expected_output: dict, key: str, allowed: set[str]) -> list[dict]:
|
|
1278
|
+
"""The entries of a per-tab expectation, checked for the shape ``layout`` and ``filter_bar`` share.
|
|
1279
|
+
|
|
1280
|
+
Raises:
|
|
1281
|
+
ValueError: not a non-empty list of objects, a key outside ``allowed``, or a tab named
|
|
1282
|
+
twice. An entry without ``tab`` is for a dashboard with one tab, so it must be the
|
|
1283
|
+
only entry: beside another, both could name that one tab.
|
|
1284
|
+
"""
|
|
1285
|
+
entries = expected_output[key]
|
|
1286
|
+
if not isinstance(entries, list) or not entries or not all(isinstance(e, dict) for e in entries):
|
|
1287
|
+
raise ValueError(f"{key} must be a non-empty list of per-tab objects, got {entries!r}")
|
|
1288
|
+
seen: set[str | None] = set()
|
|
1289
|
+
for entry in entries:
|
|
1290
|
+
unknown = sorted(entry.keys() - allowed)
|
|
1291
|
+
if unknown:
|
|
1292
|
+
raise ValueError(f"a {key} entry cannot state {unknown}, got {entry!r}")
|
|
1293
|
+
tab = entry.get("tab")
|
|
1294
|
+
if tab is not None and not (isinstance(tab, str) and tab):
|
|
1295
|
+
raise ValueError(f"a {key} entry's tab must be a tab id, got {entry!r}")
|
|
1296
|
+
if tab in seen:
|
|
1297
|
+
raise ValueError(f"{key} names tab {tab!r} more than once")
|
|
1298
|
+
seen.add(tab)
|
|
1299
|
+
if None in seen and len(entries) > 1:
|
|
1300
|
+
raise ValueError(f"a {key} entry without tab must be the only entry, got {entries!r}")
|
|
1301
|
+
return entries
|
|
1302
|
+
|
|
1060
1303
|
|
|
1061
1304
|
def _is_dataset_or_null(value: object) -> bool:
|
|
1062
1305
|
return value is None or (isinstance(value, str) and bool(value))
|
|
@@ -1103,6 +1346,8 @@ class _Applies:
|
|
|
1103
1346
|
date_filters: bool = False
|
|
1104
1347
|
preserved_widgets: bool = False
|
|
1105
1348
|
chart_types: bool = False
|
|
1349
|
+
layout: bool = False
|
|
1350
|
+
filter_bar: bool = False
|
|
1106
1351
|
|
|
1107
1352
|
@classmethod
|
|
1108
1353
|
def of(cls, expected_output: dict) -> _Applies:
|
|
@@ -1116,6 +1361,8 @@ class _Applies:
|
|
|
1116
1361
|
date_filters="date_filters" in expected_output,
|
|
1117
1362
|
preserved_widgets="preserved_widgets" in expected_output,
|
|
1118
1363
|
chart_types=_has_chart_types(expected_output),
|
|
1364
|
+
layout="layout" in expected_output,
|
|
1365
|
+
filter_bar="filter_bar" in expected_output,
|
|
1119
1366
|
)
|
|
1120
1367
|
|
|
1121
1368
|
|
|
@@ -1157,6 +1404,8 @@ class DashboardEvaluation:
|
|
|
1157
1404
|
date_filters_correct: bool = False
|
|
1158
1405
|
widgets_preserved: bool = False
|
|
1159
1406
|
chart_types_correct: bool = False
|
|
1407
|
+
layout_correct: bool = False
|
|
1408
|
+
filter_bar_correct: bool = False
|
|
1160
1409
|
failures: list[str] = field(default_factory=list)
|
|
1161
1410
|
notes: list[str] = field(default_factory=list)
|
|
1162
1411
|
|
|
@@ -1199,6 +1448,10 @@ class DashboardEvaluation:
|
|
|
1199
1448
|
checks["dashboard_widgets_preserved"] = self.widgets_preserved
|
|
1200
1449
|
if self.applies.chart_types:
|
|
1201
1450
|
checks["dashboard_chart_types_correct"] = self.chart_types_correct
|
|
1451
|
+
if self.applies.layout:
|
|
1452
|
+
checks["dashboard_layout_correct"] = self.layout_correct
|
|
1453
|
+
if self.applies.filter_bar:
|
|
1454
|
+
checks["dashboard_filter_bar_correct"] = self.filter_bar_correct
|
|
1202
1455
|
return checks
|
|
1203
1456
|
|
|
1204
1457
|
@property
|
|
@@ -1414,6 +1667,8 @@ def evaluate_dashboard_response(
|
|
|
1414
1667
|
if applies.chart_types
|
|
1415
1668
|
else ([], [])
|
|
1416
1669
|
)
|
|
1670
|
+
layout_failures = _check_layout(document, new_ids, expected_output["layout"]) if applies.layout else []
|
|
1671
|
+
filter_bar_failures = _check_filter_bar(document, expected_output["filter_bar"]) if applies.filter_bar else []
|
|
1417
1672
|
|
|
1418
1673
|
return DashboardEvaluation(
|
|
1419
1674
|
drafted=True,
|
|
@@ -1434,6 +1689,8 @@ def evaluate_dashboard_response(
|
|
|
1434
1689
|
date_filters_correct=not date_filter_failures,
|
|
1435
1690
|
widgets_preserved=not preserved_failures,
|
|
1436
1691
|
chart_types_correct=not chart_type_failures,
|
|
1692
|
+
layout_correct=not layout_failures,
|
|
1693
|
+
filter_bar_correct=not filter_bar_failures,
|
|
1437
1694
|
failures=[
|
|
1438
1695
|
*chart_failures,
|
|
1439
1696
|
*saved_failures,
|
|
@@ -1445,6 +1702,8 @@ def evaluate_dashboard_response(
|
|
|
1445
1702
|
*date_filter_failures,
|
|
1446
1703
|
*preserved_failures,
|
|
1447
1704
|
*chart_type_failures,
|
|
1705
|
+
*layout_failures,
|
|
1706
|
+
*filter_bar_failures,
|
|
1448
1707
|
*new_failures,
|
|
1449
1708
|
],
|
|
1450
1709
|
notes=[*title_notes, *reference_notes, *chart_type_notes],
|