This is page 36 of 45. Use http://codebase.md/googleapis/genai-toolbox?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .ci │ ├── continuous.release.cloudbuild.yaml │ ├── generate_release_table.sh │ ├── integration.cloudbuild.yaml │ ├── quickstart_test │ │ ├── go.integration.cloudbuild.yaml │ │ ├── js.integration.cloudbuild.yaml │ │ ├── py.integration.cloudbuild.yaml │ │ ├── run_go_tests.sh │ │ ├── run_js_tests.sh │ │ ├── run_py_tests.sh │ │ └── setup_hotels_sample.sql │ ├── test_with_coverage.sh │ └── versioned.release.cloudbuild.yaml ├── .github │ ├── auto-label.yaml │ ├── blunderbuss.yml │ ├── CODEOWNERS │ ├── header-checker-lint.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── label-sync.yml │ ├── labels.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-please.yml │ ├── renovate.json5 │ ├── sync-repo-settings.yaml │ └── workflows │ ├── cloud_build_failure_reporter.yml │ ├── deploy_dev_docs.yaml │ ├── deploy_previous_version_docs.yaml │ ├── deploy_versioned_docs.yaml │ ├── docs_deploy.yaml │ ├── docs_preview_clean.yaml │ ├── docs_preview_deploy.yaml │ ├── lint.yaml │ ├── schedule_reporter.yml │ ├── sync-labels.yaml │ └── tests.yaml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .hugo │ ├── archetypes │ │ └── default.md │ ├── assets │ │ ├── icons │ │ │ └── logo.svg │ │ └── scss │ │ ├── _styles_project.scss │ │ └── _variables_project.scss │ ├── go.mod │ ├── go.sum │ ├── hugo.toml │ ├── layouts │ │ ├── _default │ │ │ └── home.releases.releases │ │ ├── index.llms-full.txt │ │ ├── index.llms.txt │ │ ├── partials │ │ │ ├── hooks │ │ │ │ └── head-end.html │ │ │ ├── navbar-version-selector.html │ │ │ ├── page-meta-links.html │ │ │ └── td │ │ │ └── render-heading.html │ │ ├── robot.txt │ │ └── shortcodes │ │ ├── include.html │ │ ├── ipynb.html │ │ └── regionInclude.html │ ├── package-lock.json │ ├── package.json │ └── static │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico │ └── js │ └── w3.js ├── CHANGELOG.md ├── cmd │ ├── options_test.go │ ├── options.go │ ├── root_test.go │ ├── root.go │ └── version.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPER.md ├── Dockerfile ├── docs │ └── en │ ├── _index.md │ ├── about │ │ ├── _index.md │ │ └── faq.md │ ├── concepts │ │ ├── _index.md │ │ └── telemetry │ │ ├── index.md │ │ ├── telemetry_flow.png │ │ └── telemetry_traces.png │ ├── getting-started │ │ ├── _index.md │ │ ├── colab_quickstart.ipynb │ │ ├── configure.md │ │ ├── introduction │ │ │ ├── _index.md │ │ │ └── architecture.png │ │ ├── local_quickstart_go.md │ │ ├── local_quickstart_js.md │ │ ├── local_quickstart.md │ │ ├── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── quickstart │ │ ├── go │ │ │ ├── genAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── genkit │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── langchain │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ ├── openAI │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── quickstart.go │ │ │ └── quickstart_test.go │ │ ├── golden.txt │ │ ├── js │ │ │ ├── genAI │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── genkit │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── langchain │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ ├── llamaindex │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ └── quickstart.js │ │ │ └── quickstart.test.js │ │ ├── python │ │ │ ├── __init__.py │ │ │ ├── adk │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── core │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── langchain │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ ├── llamaindex │ │ │ │ ├── quickstart.py │ │ │ │ └── requirements.txt │ │ │ └── quickstart_test.py │ │ └── shared │ │ ├── cloud_setup.md │ │ ├── configure_toolbox.md │ │ └── database_setup.md │ ├── how-to │ │ ├── _index.md │ │ ├── connect_via_geminicli.md │ │ ├── connect_via_mcp.md │ │ ├── connect-ide │ │ │ ├── _index.md │ │ │ ├── alloydb_pg_admin_mcp.md │ │ │ ├── alloydb_pg_mcp.md │ │ │ ├── bigquery_mcp.md │ │ │ ├── cloud_sql_mssql_admin_mcp.md │ │ │ ├── cloud_sql_mssql_mcp.md │ │ │ ├── cloud_sql_mysql_admin_mcp.md │ │ │ ├── cloud_sql_mysql_mcp.md │ │ │ ├── cloud_sql_pg_admin_mcp.md │ │ │ ├── cloud_sql_pg_mcp.md │ │ │ ├── firestore_mcp.md │ │ │ ├── looker_mcp.md │ │ │ ├── mssql_mcp.md │ │ │ ├── mysql_mcp.md │ │ │ ├── neo4j_mcp.md │ │ │ ├── postgres_mcp.md │ │ │ ├── spanner_mcp.md │ │ │ └── sqlite_mcp.md │ │ ├── deploy_docker.md │ │ ├── deploy_gke.md │ │ ├── deploy_toolbox.md │ │ ├── export_telemetry.md │ │ └── toolbox-ui │ │ ├── edit-headers.gif │ │ ├── edit-headers.png │ │ ├── index.md │ │ ├── optional-param-checked.png │ │ ├── optional-param-unchecked.png │ │ ├── run-tool.gif │ │ ├── tools.png │ │ └── toolsets.png │ ├── reference │ │ ├── _index.md │ │ ├── cli.md │ │ └── prebuilt-tools.md │ ├── resources │ │ ├── _index.md │ │ ├── authServices │ │ │ ├── _index.md │ │ │ └── google.md │ │ ├── sources │ │ │ ├── _index.md │ │ │ ├── alloydb-admin.md │ │ │ ├── alloydb-pg.md │ │ │ ├── bigquery.md │ │ │ ├── bigtable.md │ │ │ ├── cassandra.md │ │ │ ├── clickhouse.md │ │ │ ├── cloud-monitoring.md │ │ │ ├── cloud-sql-admin.md │ │ │ ├── cloud-sql-mssql.md │ │ │ ├── cloud-sql-mysql.md │ │ │ ├── cloud-sql-pg.md │ │ │ ├── couchbase.md │ │ │ ├── dataplex.md │ │ │ ├── dgraph.md │ │ │ ├── firebird.md │ │ │ ├── firestore.md │ │ │ ├── http.md │ │ │ ├── looker.md │ │ │ ├── mongodb.md │ │ │ ├── mssql.md │ │ │ ├── mysql.md │ │ │ ├── neo4j.md │ │ │ ├── oceanbase.md │ │ │ ├── oracle.md │ │ │ ├── postgres.md │ │ │ ├── redis.md │ │ │ ├── spanner.md │ │ │ ├── sqlite.md │ │ │ ├── tidb.md │ │ │ ├── trino.md │ │ │ ├── valkey.md │ │ │ └── yugabytedb.md │ │ └── tools │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── alloydb-create-cluster.md │ │ │ ├── alloydb-create-instance.md │ │ │ ├── alloydb-create-user.md │ │ │ ├── alloydb-get-cluster.md │ │ │ ├── alloydb-get-instance.md │ │ │ ├── alloydb-get-user.md │ │ │ ├── alloydb-list-clusters.md │ │ │ ├── alloydb-list-instances.md │ │ │ ├── alloydb-list-users.md │ │ │ └── alloydb-wait-for-operation.md │ │ ├── alloydbainl │ │ │ ├── _index.md │ │ │ └── alloydb-ai-nl.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── bigquery-analyze-contribution.md │ │ │ ├── bigquery-conversational-analytics.md │ │ │ ├── bigquery-execute-sql.md │ │ │ ├── bigquery-forecast.md │ │ │ ├── bigquery-get-dataset-info.md │ │ │ ├── bigquery-get-table-info.md │ │ │ ├── bigquery-list-dataset-ids.md │ │ │ ├── bigquery-list-table-ids.md │ │ │ ├── bigquery-search-catalog.md │ │ │ └── bigquery-sql.md │ │ ├── bigtable │ │ │ ├── _index.md │ │ │ └── bigtable-sql.md │ │ ├── cassandra │ │ │ ├── _index.md │ │ │ └── cassandra-cql.md │ │ ├── clickhouse │ │ │ ├── _index.md │ │ │ ├── clickhouse-execute-sql.md │ │ │ ├── clickhouse-list-databases.md │ │ │ ├── clickhouse-list-tables.md │ │ │ └── clickhouse-sql.md │ │ ├── cloudmonitoring │ │ │ ├── _index.md │ │ │ └── cloud-monitoring-query-prometheus.md │ │ ├── cloudsql │ │ │ ├── _index.md │ │ │ ├── cloudsqlcreatedatabase.md │ │ │ ├── cloudsqlcreateusers.md │ │ │ ├── cloudsqlgetinstances.md │ │ │ ├── cloudsqllistdatabases.md │ │ │ ├── cloudsqllistinstances.md │ │ │ ├── cloudsqlmssqlcreateinstance.md │ │ │ ├── cloudsqlmysqlcreateinstance.md │ │ │ ├── cloudsqlpgcreateinstances.md │ │ │ └── cloudsqlwaitforoperation.md │ │ ├── couchbase │ │ │ ├── _index.md │ │ │ └── couchbase-sql.md │ │ ├── dataform │ │ │ ├── _index.md │ │ │ └── dataform-compile-local.md │ │ ├── dataplex │ │ │ ├── _index.md │ │ │ ├── dataplex-lookup-entry.md │ │ │ ├── dataplex-search-aspect-types.md │ │ │ └── dataplex-search-entries.md │ │ ├── dgraph │ │ │ ├── _index.md │ │ │ └── dgraph-dql.md │ │ ├── firebird │ │ │ ├── _index.md │ │ │ ├── firebird-execute-sql.md │ │ │ └── firebird-sql.md │ │ ├── firestore │ │ │ ├── _index.md │ │ │ ├── firestore-add-documents.md │ │ │ ├── firestore-delete-documents.md │ │ │ ├── firestore-get-documents.md │ │ │ ├── firestore-get-rules.md │ │ │ ├── firestore-list-collections.md │ │ │ ├── firestore-query-collection.md │ │ │ ├── firestore-query.md │ │ │ ├── firestore-update-document.md │ │ │ └── firestore-validate-rules.md │ │ ├── http │ │ │ ├── _index.md │ │ │ └── http.md │ │ ├── looker │ │ │ ├── _index.md │ │ │ ├── looker-add-dashboard-element.md │ │ │ ├── looker-conversational-analytics.md │ │ │ ├── looker-get-dashboards.md │ │ │ ├── looker-get-dimensions.md │ │ │ ├── looker-get-explores.md │ │ │ ├── looker-get-filters.md │ │ │ ├── looker-get-looks.md │ │ │ ├── looker-get-measures.md │ │ │ ├── looker-get-models.md │ │ │ ├── looker-get-parameters.md │ │ │ ├── looker-health-analyze.md │ │ │ ├── looker-health-pulse.md │ │ │ ├── looker-health-vacuum.md │ │ │ ├── looker-make-dashboard.md │ │ │ ├── looker-make-look.md │ │ │ ├── looker-query-sql.md │ │ │ ├── looker-query-url.md │ │ │ ├── looker-query.md │ │ │ └── looker-run-look.md │ │ ├── mongodb │ │ │ ├── _index.md │ │ │ ├── mongodb-aggregate.md │ │ │ ├── mongodb-delete-many.md │ │ │ ├── mongodb-delete-one.md │ │ │ ├── mongodb-find-one.md │ │ │ ├── mongodb-find.md │ │ │ ├── mongodb-insert-many.md │ │ │ ├── mongodb-insert-one.md │ │ │ ├── mongodb-update-many.md │ │ │ └── mongodb-update-one.md │ │ ├── mssql │ │ │ ├── _index.md │ │ │ ├── mssql-execute-sql.md │ │ │ ├── mssql-list-tables.md │ │ │ └── mssql-sql.md │ │ ├── mysql │ │ │ ├── _index.md │ │ │ ├── mysql-execute-sql.md │ │ │ ├── mysql-list-active-queries.md │ │ │ ├── mysql-list-table-fragmentation.md │ │ │ ├── mysql-list-tables-missing-unique-indexes.md │ │ │ ├── mysql-list-tables.md │ │ │ └── mysql-sql.md │ │ ├── neo4j │ │ │ ├── _index.md │ │ │ ├── neo4j-cypher.md │ │ │ ├── neo4j-execute-cypher.md │ │ │ └── neo4j-schema.md │ │ ├── oceanbase │ │ │ ├── _index.md │ │ │ ├── oceanbase-execute-sql.md │ │ │ └── oceanbase-sql.md │ │ ├── oracle │ │ │ ├── _index.md │ │ │ ├── oracle-execute-sql.md │ │ │ └── oracle-sql.md │ │ ├── postgres │ │ │ ├── _index.md │ │ │ ├── postgres-execute-sql.md │ │ │ ├── postgres-list-active-queries.md │ │ │ ├── postgres-list-available-extensions.md │ │ │ ├── postgres-list-installed-extensions.md │ │ │ ├── postgres-list-tables.md │ │ │ └── postgres-sql.md │ │ ├── redis │ │ │ ├── _index.md │ │ │ └── redis.md │ │ ├── spanner │ │ │ ├── _index.md │ │ │ ├── spanner-execute-sql.md │ │ │ ├── spanner-list-tables.md │ │ │ └── spanner-sql.md │ │ ├── sqlite │ │ │ ├── _index.md │ │ │ ├── sqlite-execute-sql.md │ │ │ └── sqlite-sql.md │ │ ├── tidb │ │ │ ├── _index.md │ │ │ ├── tidb-execute-sql.md │ │ │ └── tidb-sql.md │ │ ├── trino │ │ │ ├── _index.md │ │ │ ├── trino-execute-sql.md │ │ │ └── trino-sql.md │ │ ├── utility │ │ │ ├── _index.md │ │ │ └── wait.md │ │ ├── valkey │ │ │ ├── _index.md │ │ │ └── valkey.md │ │ └── yuagbytedb │ │ ├── _index.md │ │ └── yugabytedb-sql.md │ ├── samples │ │ ├── _index.md │ │ ├── alloydb │ │ │ ├── _index.md │ │ │ ├── ai-nl │ │ │ │ ├── alloydb_ai_nl.ipynb │ │ │ │ └── index.md │ │ │ └── mcp_quickstart.md │ │ ├── bigquery │ │ │ ├── _index.md │ │ │ ├── colab_quickstart_bigquery.ipynb │ │ │ ├── local_quickstart.md │ │ │ └── mcp_quickstart │ │ │ ├── _index.md │ │ │ ├── inspector_tools.png │ │ │ └── inspector.png │ │ └── looker │ │ ├── _index.md │ │ ├── looker_gemini_oauth │ │ │ ├── _index.md │ │ │ ├── authenticated.png │ │ │ ├── authorize.png │ │ │ └── registration.png │ │ ├── looker_gemini.md │ │ └── looker_mcp_inspector │ │ ├── _index.md │ │ ├── inspector_tools.png │ │ └── inspector.png │ └── sdks │ ├── _index.md │ ├── go-sdk.md │ ├── js-sdk.md │ └── python-sdk.md ├── gemini-extension.json ├── go.mod ├── go.sum ├── internal │ ├── auth │ │ ├── auth.go │ │ └── google │ │ └── google.go │ ├── log │ │ ├── handler.go │ │ ├── log_test.go │ │ ├── log.go │ │ └── logger.go │ ├── prebuiltconfigs │ │ ├── prebuiltconfigs_test.go │ │ ├── prebuiltconfigs.go │ │ └── tools │ │ ├── alloydb-postgres-admin.yaml │ │ ├── alloydb-postgres-observability.yaml │ │ ├── alloydb-postgres.yaml │ │ ├── bigquery.yaml │ │ ├── clickhouse.yaml │ │ ├── cloud-sql-mssql-admin.yaml │ │ ├── cloud-sql-mssql-observability.yaml │ │ ├── cloud-sql-mssql.yaml │ │ ├── cloud-sql-mysql-admin.yaml │ │ ├── cloud-sql-mysql-observability.yaml │ │ ├── cloud-sql-mysql.yaml │ │ ├── cloud-sql-postgres-admin.yaml │ │ ├── cloud-sql-postgres-observability.yaml │ │ ├── cloud-sql-postgres.yaml │ │ ├── dataplex.yaml │ │ ├── firestore.yaml │ │ ├── looker-conversational-analytics.yaml │ │ ├── looker.yaml │ │ ├── mssql.yaml │ │ ├── mysql.yaml │ │ ├── neo4j.yaml │ │ ├── oceanbase.yaml │ │ ├── postgres.yaml │ │ ├── spanner-postgres.yaml │ │ ├── spanner.yaml │ │ └── sqlite.yaml │ ├── server │ │ ├── api_test.go │ │ ├── api.go │ │ ├── common_test.go │ │ ├── config.go │ │ ├── mcp │ │ │ ├── jsonrpc │ │ │ │ ├── jsonrpc_test.go │ │ │ │ └── jsonrpc.go │ │ │ ├── mcp.go │ │ │ ├── util │ │ │ │ └── lifecycle.go │ │ │ ├── v20241105 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ ├── v20250326 │ │ │ │ ├── method.go │ │ │ │ └── types.go │ │ │ └── v20250618 │ │ │ ├── method.go │ │ │ └── types.go │ │ ├── mcp_test.go │ │ ├── mcp.go │ │ ├── server_test.go │ │ ├── server.go │ │ ├── static │ │ │ ├── assets │ │ │ │ └── mcptoolboxlogo.png │ │ │ ├── css │ │ │ │ └── style.css │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── auth.js │ │ │ │ ├── loadTools.js │ │ │ │ ├── mainContent.js │ │ │ │ ├── navbar.js │ │ │ │ ├── runTool.js │ │ │ │ ├── toolDisplay.js │ │ │ │ ├── tools.js │ │ │ │ └── toolsets.js │ │ │ ├── tools.html │ │ │ └── toolsets.html │ │ ├── web_test.go │ │ └── web.go │ ├── sources │ │ ├── alloydbadmin │ │ │ ├── alloydbadmin_test.go │ │ │ └── alloydbadmin.go │ │ ├── alloydbpg │ │ │ ├── alloydb_pg_test.go │ │ │ └── alloydb_pg.go │ │ ├── bigquery │ │ │ ├── bigquery_test.go │ │ │ └── bigquery.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ ├── cassandra_test.go │ │ │ └── cassandra.go │ │ ├── clickhouse │ │ │ ├── clickhouse_test.go │ │ │ └── clickhouse.go │ │ ├── cloudmonitoring │ │ │ ├── cloud_monitoring_test.go │ │ │ └── cloud_monitoring.go │ │ ├── cloudsqladmin │ │ │ ├── cloud_sql_admin_test.go │ │ │ └── cloud_sql_admin.go │ │ ├── cloudsqlmssql │ │ │ ├── cloud_sql_mssql_test.go │ │ │ └── cloud_sql_mssql.go │ │ ├── cloudsqlmysql │ │ │ ├── cloud_sql_mysql_test.go │ │ │ └── cloud_sql_mysql.go │ │ ├── cloudsqlpg │ │ │ ├── cloud_sql_pg_test.go │ │ │ └── cloud_sql_pg.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataplex │ │ │ ├── dataplex_test.go │ │ │ └── dataplex.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── dialect.go │ │ ├── firebird │ │ │ ├── firebird_test.go │ │ │ └── firebird.go │ │ ├── firestore │ │ │ ├── firestore_test.go │ │ │ └── firestore.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── ip_type.go │ │ ├── looker │ │ │ ├── looker_test.go │ │ │ └── looker.go │ │ ├── mongodb │ │ │ ├── mongodb_test.go │ │ │ └── mongodb.go │ │ ├── mssql │ │ │ ├── mssql_test.go │ │ │ └── mssql.go │ │ ├── mysql │ │ │ ├── mysql_test.go │ │ │ └── mysql.go │ │ ├── neo4j │ │ │ ├── neo4j_test.go │ │ │ └── neo4j.go │ │ ├── oceanbase │ │ │ ├── oceanbase_test.go │ │ │ └── oceanbase.go │ │ ├── oracle │ │ │ └── oracle.go │ │ ├── postgres │ │ │ ├── postgres_test.go │ │ │ └── postgres.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── sources.go │ │ ├── spanner │ │ │ ├── spanner_test.go │ │ │ └── spanner.go │ │ ├── sqlite │ │ │ ├── sqlite_test.go │ │ │ └── sqlite.go │ │ ├── tidb │ │ │ ├── tidb_test.go │ │ │ └── tidb.go │ │ ├── trino │ │ │ ├── trino_test.go │ │ │ └── trino.go │ │ ├── util.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedb │ │ ├── yugabytedb_test.go │ │ └── yugabytedb.go │ ├── telemetry │ │ ├── instrumentation.go │ │ └── telemetry.go │ ├── testutils │ │ └── testutils.go │ ├── tools │ │ ├── alloydb │ │ │ ├── alloydbcreatecluster │ │ │ │ ├── alloydbcreatecluster_test.go │ │ │ │ └── alloydbcreatecluster.go │ │ │ ├── alloydbcreateinstance │ │ │ │ ├── alloydbcreateinstance_test.go │ │ │ │ └── alloydbcreateinstance.go │ │ │ ├── alloydbcreateuser │ │ │ │ ├── alloydbcreateuser_test.go │ │ │ │ └── alloydbcreateuser.go │ │ │ ├── alloydbgetcluster │ │ │ │ ├── alloydbgetcluster_test.go │ │ │ │ └── alloydbgetcluster.go │ │ │ ├── alloydbgetinstance │ │ │ │ ├── alloydbgetinstance_test.go │ │ │ │ └── alloydbgetinstance.go │ │ │ ├── alloydbgetuser │ │ │ │ ├── alloydbgetuser_test.go │ │ │ │ └── alloydbgetuser.go │ │ │ ├── alloydblistclusters │ │ │ │ ├── alloydblistclusters_test.go │ │ │ │ └── alloydblistclusters.go │ │ │ ├── alloydblistinstances │ │ │ │ ├── alloydblistinstances_test.go │ │ │ │ └── alloydblistinstances.go │ │ │ ├── alloydblistusers │ │ │ │ ├── alloydblistusers_test.go │ │ │ │ └── alloydblistusers.go │ │ │ └── alloydbwaitforoperation │ │ │ ├── alloydbwaitforoperation_test.go │ │ │ └── alloydbwaitforoperation.go │ │ ├── alloydbainl │ │ │ ├── alloydbainl_test.go │ │ │ └── alloydbainl.go │ │ ├── bigquery │ │ │ ├── bigqueryanalyzecontribution │ │ │ │ ├── bigqueryanalyzecontribution_test.go │ │ │ │ └── bigqueryanalyzecontribution.go │ │ │ ├── bigquerycommon │ │ │ │ ├── table_name_parser_test.go │ │ │ │ ├── table_name_parser.go │ │ │ │ └── util.go │ │ │ ├── bigqueryconversationalanalytics │ │ │ │ ├── bigqueryconversationalanalytics_test.go │ │ │ │ └── bigqueryconversationalanalytics.go │ │ │ ├── bigqueryexecutesql │ │ │ │ ├── bigqueryexecutesql_test.go │ │ │ │ └── bigqueryexecutesql.go │ │ │ ├── bigqueryforecast │ │ │ │ ├── bigqueryforecast_test.go │ │ │ │ └── bigqueryforecast.go │ │ │ ├── bigquerygetdatasetinfo │ │ │ │ ├── bigquerygetdatasetinfo_test.go │ │ │ │ └── bigquerygetdatasetinfo.go │ │ │ ├── bigquerygettableinfo │ │ │ │ ├── bigquerygettableinfo_test.go │ │ │ │ └── bigquerygettableinfo.go │ │ │ ├── bigquerylistdatasetids │ │ │ │ ├── bigquerylistdatasetids_test.go │ │ │ │ └── bigquerylistdatasetids.go │ │ │ ├── bigquerylisttableids │ │ │ │ ├── bigquerylisttableids_test.go │ │ │ │ └── bigquerylisttableids.go │ │ │ ├── bigquerysearchcatalog │ │ │ │ ├── bigquerysearchcatalog_test.go │ │ │ │ └── bigquerysearchcatalog.go │ │ │ └── bigquerysql │ │ │ ├── bigquerysql_test.go │ │ │ └── bigquerysql.go │ │ ├── bigtable │ │ │ ├── bigtable_test.go │ │ │ └── bigtable.go │ │ ├── cassandra │ │ │ └── cassandracql │ │ │ ├── cassandracql_test.go │ │ │ └── cassandracql.go │ │ ├── clickhouse │ │ │ ├── clickhouseexecutesql │ │ │ │ ├── clickhouseexecutesql_test.go │ │ │ │ └── clickhouseexecutesql.go │ │ │ ├── clickhouselistdatabases │ │ │ │ ├── clickhouselistdatabases_test.go │ │ │ │ └── clickhouselistdatabases.go │ │ │ ├── clickhouselisttables │ │ │ │ ├── clickhouselisttables_test.go │ │ │ │ └── clickhouselisttables.go │ │ │ └── clickhousesql │ │ │ ├── clickhousesql_test.go │ │ │ └── clickhousesql.go │ │ ├── cloudmonitoring │ │ │ ├── cloudmonitoring_test.go │ │ │ └── cloudmonitoring.go │ │ ├── cloudsql │ │ │ ├── cloudsqlcreatedatabase │ │ │ │ ├── cloudsqlcreatedatabase_test.go │ │ │ │ └── cloudsqlcreatedatabase.go │ │ │ ├── cloudsqlcreateusers │ │ │ │ ├── cloudsqlcreateusers_test.go │ │ │ │ └── cloudsqlcreateusers.go │ │ │ ├── cloudsqlgetinstances │ │ │ │ ├── cloudsqlgetinstances_test.go │ │ │ │ └── cloudsqlgetinstances.go │ │ │ ├── cloudsqllistdatabases │ │ │ │ ├── cloudsqllistdatabases_test.go │ │ │ │ └── cloudsqllistdatabases.go │ │ │ ├── cloudsqllistinstances │ │ │ │ ├── cloudsqllistinstances_test.go │ │ │ │ └── cloudsqllistinstances.go │ │ │ └── cloudsqlwaitforoperation │ │ │ ├── cloudsqlwaitforoperation_test.go │ │ │ └── cloudsqlwaitforoperation.go │ │ ├── cloudsqlmssql │ │ │ └── cloudsqlmssqlcreateinstance │ │ │ ├── cloudsqlmssqlcreateinstance_test.go │ │ │ └── cloudsqlmssqlcreateinstance.go │ │ ├── cloudsqlmysql │ │ │ └── cloudsqlmysqlcreateinstance │ │ │ ├── cloudsqlmysqlcreateinstance_test.go │ │ │ └── cloudsqlmysqlcreateinstance.go │ │ ├── cloudsqlpg │ │ │ └── cloudsqlpgcreateinstances │ │ │ ├── cloudsqlpgcreateinstances_test.go │ │ │ └── cloudsqlpgcreateinstances.go │ │ ├── common_test.go │ │ ├── common.go │ │ ├── couchbase │ │ │ ├── couchbase_test.go │ │ │ └── couchbase.go │ │ ├── dataform │ │ │ └── dataformcompilelocal │ │ │ ├── dataformcompilelocal_test.go │ │ │ └── dataformcompilelocal.go │ │ ├── dataplex │ │ │ ├── dataplexlookupentry │ │ │ │ ├── dataplexlookupentry_test.go │ │ │ │ └── dataplexlookupentry.go │ │ │ ├── dataplexsearchaspecttypes │ │ │ │ ├── dataplexsearchaspecttypes_test.go │ │ │ │ └── dataplexsearchaspecttypes.go │ │ │ └── dataplexsearchentries │ │ │ ├── dataplexsearchentries_test.go │ │ │ └── dataplexsearchentries.go │ │ ├── dgraph │ │ │ ├── dgraph_test.go │ │ │ └── dgraph.go │ │ ├── firebird │ │ │ ├── firebirdexecutesql │ │ │ │ ├── firebirdexecutesql_test.go │ │ │ │ └── firebirdexecutesql.go │ │ │ └── firebirdsql │ │ │ ├── firebirdsql_test.go │ │ │ └── firebirdsql.go │ │ ├── firestore │ │ │ ├── firestoreadddocuments │ │ │ │ ├── firestoreadddocuments_test.go │ │ │ │ └── firestoreadddocuments.go │ │ │ ├── firestoredeletedocuments │ │ │ │ ├── firestoredeletedocuments_test.go │ │ │ │ └── firestoredeletedocuments.go │ │ │ ├── firestoregetdocuments │ │ │ │ ├── firestoregetdocuments_test.go │ │ │ │ └── firestoregetdocuments.go │ │ │ ├── firestoregetrules │ │ │ │ ├── firestoregetrules_test.go │ │ │ │ └── firestoregetrules.go │ │ │ ├── firestorelistcollections │ │ │ │ ├── firestorelistcollections_test.go │ │ │ │ └── firestorelistcollections.go │ │ │ ├── firestorequery │ │ │ │ ├── firestorequery_test.go │ │ │ │ └── firestorequery.go │ │ │ ├── firestorequerycollection │ │ │ │ ├── firestorequerycollection_test.go │ │ │ │ └── firestorequerycollection.go │ │ │ ├── firestoreupdatedocument │ │ │ │ ├── firestoreupdatedocument_test.go │ │ │ │ └── firestoreupdatedocument.go │ │ │ ├── firestorevalidaterules │ │ │ │ ├── firestorevalidaterules_test.go │ │ │ │ └── firestorevalidaterules.go │ │ │ └── util │ │ │ ├── converter_test.go │ │ │ ├── converter.go │ │ │ ├── validator_test.go │ │ │ └── validator.go │ │ ├── http │ │ │ ├── http_test.go │ │ │ └── http.go │ │ ├── http_method.go │ │ ├── looker │ │ │ ├── lookeradddashboardelement │ │ │ │ ├── lookeradddashboardelement_test.go │ │ │ │ └── lookeradddashboardelement.go │ │ │ ├── lookercommon │ │ │ │ ├── lookercommon_test.go │ │ │ │ └── lookercommon.go │ │ │ ├── lookerconversationalanalytics │ │ │ │ ├── lookerconversationalanalytics_test.go │ │ │ │ └── lookerconversationalanalytics.go │ │ │ ├── lookergetdashboards │ │ │ │ ├── lookergetdashboards_test.go │ │ │ │ └── lookergetdashboards.go │ │ │ ├── lookergetdimensions │ │ │ │ ├── lookergetdimensions_test.go │ │ │ │ └── lookergetdimensions.go │ │ │ ├── lookergetexplores │ │ │ │ ├── lookergetexplores_test.go │ │ │ │ └── lookergetexplores.go │ │ │ ├── lookergetfilters │ │ │ │ ├── lookergetfilters_test.go │ │ │ │ └── lookergetfilters.go │ │ │ ├── lookergetlooks │ │ │ │ ├── lookergetlooks_test.go │ │ │ │ └── lookergetlooks.go │ │ │ ├── lookergetmeasures │ │ │ │ ├── lookergetmeasures_test.go │ │ │ │ └── lookergetmeasures.go │ │ │ ├── lookergetmodels │ │ │ │ ├── lookergetmodels_test.go │ │ │ │ └── lookergetmodels.go │ │ │ ├── lookergetparameters │ │ │ │ ├── lookergetparameters_test.go │ │ │ │ └── lookergetparameters.go │ │ │ ├── lookerhealthanalyze │ │ │ │ ├── lookerhealthanalyze_test.go │ │ │ │ └── lookerhealthanalyze.go │ │ │ ├── lookerhealthpulse │ │ │ │ ├── lookerhealthpulse_test.go │ │ │ │ └── lookerhealthpulse.go │ │ │ ├── lookerhealthvacuum │ │ │ │ ├── lookerhealthvacuum_test.go │ │ │ │ └── lookerhealthvacuum.go │ │ │ ├── lookermakedashboard │ │ │ │ ├── lookermakedashboard_test.go │ │ │ │ └── lookermakedashboard.go │ │ │ ├── lookermakelook │ │ │ │ ├── lookermakelook_test.go │ │ │ │ └── lookermakelook.go │ │ │ ├── lookerquery │ │ │ │ ├── lookerquery_test.go │ │ │ │ └── lookerquery.go │ │ │ ├── lookerquerysql │ │ │ │ ├── lookerquerysql_test.go │ │ │ │ └── lookerquerysql.go │ │ │ ├── lookerqueryurl │ │ │ │ ├── lookerqueryurl_test.go │ │ │ │ └── lookerqueryurl.go │ │ │ └── lookerrunlook │ │ │ ├── lookerrunlook_test.go │ │ │ └── lookerrunlook.go │ │ ├── mongodb │ │ │ ├── mongodbaggregate │ │ │ │ ├── mongodbaggregate_test.go │ │ │ │ └── mongodbaggregate.go │ │ │ ├── mongodbdeletemany │ │ │ │ ├── mongodbdeletemany_test.go │ │ │ │ └── mongodbdeletemany.go │ │ │ ├── mongodbdeleteone │ │ │ │ ├── mongodbdeleteone_test.go │ │ │ │ └── mongodbdeleteone.go │ │ │ ├── mongodbfind │ │ │ │ ├── mongodbfind_test.go │ │ │ │ └── mongodbfind.go │ │ │ ├── mongodbfindone │ │ │ │ ├── mongodbfindone_test.go │ │ │ │ └── mongodbfindone.go │ │ │ ├── mongodbinsertmany │ │ │ │ ├── mongodbinsertmany_test.go │ │ │ │ └── mongodbinsertmany.go │ │ │ ├── mongodbinsertone │ │ │ │ ├── mongodbinsertone_test.go │ │ │ │ └── mongodbinsertone.go │ │ │ ├── mongodbupdatemany │ │ │ │ ├── mongodbupdatemany_test.go │ │ │ │ └── mongodbupdatemany.go │ │ │ └── mongodbupdateone │ │ │ ├── mongodbupdateone_test.go │ │ │ └── mongodbupdateone.go │ │ ├── mssql │ │ │ ├── mssqlexecutesql │ │ │ │ ├── mssqlexecutesql_test.go │ │ │ │ └── mssqlexecutesql.go │ │ │ ├── mssqllisttables │ │ │ │ ├── mssqllisttables_test.go │ │ │ │ └── mssqllisttables.go │ │ │ └── mssqlsql │ │ │ ├── mssqlsql_test.go │ │ │ └── mssqlsql.go │ │ ├── mysql │ │ │ ├── mysqlcommon │ │ │ │ └── mysqlcommon.go │ │ │ ├── mysqlexecutesql │ │ │ │ ├── mysqlexecutesql_test.go │ │ │ │ └── mysqlexecutesql.go │ │ │ ├── mysqllistactivequeries │ │ │ │ ├── mysqllistactivequeries_test.go │ │ │ │ └── mysqllistactivequeries.go │ │ │ ├── mysqllisttablefragmentation │ │ │ │ ├── mysqllisttablefragmentation_test.go │ │ │ │ └── mysqllisttablefragmentation.go │ │ │ ├── mysqllisttables │ │ │ │ ├── mysqllisttables_test.go │ │ │ │ └── mysqllisttables.go │ │ │ ├── mysqllisttablesmissinguniqueindexes │ │ │ │ ├── mysqllisttablesmissinguniqueindexes_test.go │ │ │ │ └── mysqllisttablesmissinguniqueindexes.go │ │ │ └── mysqlsql │ │ │ ├── mysqlsql_test.go │ │ │ └── mysqlsql.go │ │ ├── neo4j │ │ │ ├── neo4jcypher │ │ │ │ ├── neo4jcypher_test.go │ │ │ │ └── neo4jcypher.go │ │ │ ├── neo4jexecutecypher │ │ │ │ ├── classifier │ │ │ │ │ ├── classifier_test.go │ │ │ │ │ └── classifier.go │ │ │ │ ├── neo4jexecutecypher_test.go │ │ │ │ └── neo4jexecutecypher.go │ │ │ └── neo4jschema │ │ │ ├── cache │ │ │ │ ├── cache_test.go │ │ │ │ └── cache.go │ │ │ ├── helpers │ │ │ │ ├── helpers_test.go │ │ │ │ └── helpers.go │ │ │ ├── neo4jschema_test.go │ │ │ ├── neo4jschema.go │ │ │ └── types │ │ │ └── types.go │ │ ├── oceanbase │ │ │ ├── oceanbaseexecutesql │ │ │ │ ├── oceanbaseexecutesql_test.go │ │ │ │ └── oceanbaseexecutesql.go │ │ │ └── oceanbasesql │ │ │ ├── oceanbasesql_test.go │ │ │ └── oceanbasesql.go │ │ ├── oracle │ │ │ ├── oracleexecutesql │ │ │ │ └── oracleexecutesql.go │ │ │ └── oraclesql │ │ │ └── oraclesql.go │ │ ├── parameters_test.go │ │ ├── parameters.go │ │ ├── postgres │ │ │ ├── postgresexecutesql │ │ │ │ ├── postgresexecutesql_test.go │ │ │ │ └── postgresexecutesql.go │ │ │ ├── postgreslistactivequeries │ │ │ │ ├── postgreslistactivequeries_test.go │ │ │ │ └── postgreslistactivequeries.go │ │ │ ├── postgreslistavailableextensions │ │ │ │ ├── postgreslistavailableextensions_test.go │ │ │ │ └── postgreslistavailableextensions.go │ │ │ ├── postgreslistinstalledextensions │ │ │ │ ├── postgreslistinstalledextensions_test.go │ │ │ │ └── postgreslistinstalledextensions.go │ │ │ ├── postgreslisttables │ │ │ │ ├── postgreslisttables_test.go │ │ │ │ └── postgreslisttables.go │ │ │ └── postgressql │ │ │ ├── postgressql_test.go │ │ │ └── postgressql.go │ │ ├── redis │ │ │ ├── redis_test.go │ │ │ └── redis.go │ │ ├── spanner │ │ │ ├── spannerexecutesql │ │ │ │ ├── spannerexecutesql_test.go │ │ │ │ └── spannerexecutesql.go │ │ │ ├── spannerlisttables │ │ │ │ ├── spannerlisttables_test.go │ │ │ │ └── spannerlisttables.go │ │ │ └── spannersql │ │ │ ├── spanner_test.go │ │ │ └── spannersql.go │ │ ├── sqlite │ │ │ ├── sqliteexecutesql │ │ │ │ ├── sqliteexecutesql_test.go │ │ │ │ └── sqliteexecutesql.go │ │ │ └── sqlitesql │ │ │ ├── sqlitesql_test.go │ │ │ └── sqlitesql.go │ │ ├── tidb │ │ │ ├── tidbexecutesql │ │ │ │ ├── tidbexecutesql_test.go │ │ │ │ └── tidbexecutesql.go │ │ │ └── tidbsql │ │ │ ├── tidbsql_test.go │ │ │ └── tidbsql.go │ │ ├── tools_test.go │ │ ├── tools.go │ │ ├── toolsets.go │ │ ├── trino │ │ │ ├── trinoexecutesql │ │ │ │ ├── trinoexecutesql_test.go │ │ │ │ └── trinoexecutesql.go │ │ │ └── trinosql │ │ │ ├── trinosql_test.go │ │ │ └── trinosql.go │ │ ├── utility │ │ │ └── wait │ │ │ ├── wait_test.go │ │ │ └── wait.go │ │ ├── valkey │ │ │ ├── valkey_test.go │ │ │ └── valkey.go │ │ └── yugabytedbsql │ │ ├── yugabytedbsql_test.go │ │ └── yugabytedbsql.go │ └── util │ └── util.go ├── LICENSE ├── logo.png ├── main.go ├── MCP-TOOLBOX-EXTENSION.md ├── README.md └── tests ├── alloydb │ ├── alloydb_integration_test.go │ └── alloydb_wait_for_operation_test.go ├── alloydbainl │ └── alloydb_ai_nl_integration_test.go ├── alloydbpg │ └── alloydb_pg_integration_test.go ├── auth.go ├── bigquery │ └── bigquery_integration_test.go ├── bigtable │ └── bigtable_integration_test.go ├── cassandra │ └── cassandra_integration_test.go ├── clickhouse │ └── clickhouse_integration_test.go ├── cloudmonitoring │ └── cloud_monitoring_integration_test.go ├── cloudsql │ ├── cloud_sql_create_database_test.go │ ├── cloud_sql_create_users_test.go │ ├── cloud_sql_get_instances_test.go │ ├── cloud_sql_list_databases_test.go │ ├── cloudsql_list_instances_test.go │ └── cloudsql_wait_for_operation_test.go ├── cloudsqlmssql │ ├── cloud_sql_mssql_create_instance_integration_test.go │ └── cloud_sql_mssql_integration_test.go ├── cloudsqlmysql │ ├── cloud_sql_mysql_create_instance_integration_test.go │ └── cloud_sql_mysql_integration_test.go ├── cloudsqlpg │ ├── cloud_sql_pg_create_instances_test.go │ └── cloud_sql_pg_integration_test.go ├── common.go ├── couchbase │ └── couchbase_integration_test.go ├── dataform │ └── dataform_integration_test.go ├── dataplex │ └── dataplex_integration_test.go ├── dgraph │ └── dgraph_integration_test.go ├── firebird │ └── firebird_integration_test.go ├── firestore │ └── firestore_integration_test.go ├── http │ └── http_integration_test.go ├── looker │ └── looker_integration_test.go ├── mongodb │ └── mongodb_integration_test.go ├── mssql │ └── mssql_integration_test.go ├── mysql │ └── mysql_integration_test.go ├── neo4j │ └── neo4j_integration_test.go ├── oceanbase │ └── oceanbase_integration_test.go ├── option.go ├── oracle │ └── oracle_integration_test.go ├── postgres │ └── postgres_integration_test.go ├── redis │ └── redis_test.go ├── server.go ├── source.go ├── spanner │ └── spanner_integration_test.go ├── sqlite │ └── sqlite_integration_test.go ├── tidb │ └── tidb_integration_test.go ├── tool.go ├── trino │ └── trino_integration_test.go ├── utility │ └── wait_integration_test.go ├── valkey │ └── valkey_test.go └── yugabytedb └── yugabytedb_integration_test.go ``` # Files -------------------------------------------------------------------------------- /tests/dataplex/dataplex_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package dataplex 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "io" 23 | "net/http" 24 | "os" 25 | "regexp" 26 | "strings" 27 | "testing" 28 | "time" 29 | 30 | bigqueryapi "cloud.google.com/go/bigquery" 31 | dataplex "cloud.google.com/go/dataplex/apiv1" 32 | dataplexpb "cloud.google.com/go/dataplex/apiv1/dataplexpb" 33 | "github.com/google/uuid" 34 | "github.com/googleapis/genai-toolbox/internal/testutils" 35 | "github.com/googleapis/genai-toolbox/tests" 36 | "golang.org/x/oauth2/google" 37 | "google.golang.org/api/googleapi" 38 | "google.golang.org/api/iterator" 39 | "google.golang.org/api/option" 40 | ) 41 | 42 | var ( 43 | DataplexSourceKind = "dataplex" 44 | DataplexSearchEntriesToolKind = "dataplex-search-entries" 45 | DataplexLookupEntryToolKind = "dataplex-lookup-entry" 46 | DataplexSearchAspectTypesToolKind = "dataplex-search-aspect-types" 47 | DataplexProject = os.Getenv("DATAPLEX_PROJECT") 48 | ) 49 | 50 | func getDataplexVars(t *testing.T) map[string]any { 51 | switch "" { 52 | case DataplexProject: 53 | t.Fatal("'DATAPLEX_PROJECT' not set") 54 | } 55 | return map[string]any{ 56 | "kind": DataplexSourceKind, 57 | "project": DataplexProject, 58 | } 59 | } 60 | 61 | // Copied over from bigquery.go 62 | func initBigQueryConnection(ctx context.Context, project string) (*bigqueryapi.Client, error) { 63 | cred, err := google.FindDefaultCredentials(ctx, bigqueryapi.Scope) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", bigqueryapi.Scope, err) 66 | } 67 | 68 | client, err := bigqueryapi.NewClient(ctx, project, option.WithCredentials(cred)) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to create BigQuery client for project %q: %w", project, err) 71 | } 72 | return client, nil 73 | } 74 | 75 | func initDataplexConnection(ctx context.Context) (*dataplex.CatalogClient, error) { 76 | cred, err := google.FindDefaultCredentials(ctx) 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to find default Google Cloud credentials: %w", err) 79 | } 80 | 81 | client, err := dataplex.NewCatalogClient(ctx, option.WithCredentials(cred)) 82 | if err != nil { 83 | return nil, fmt.Errorf("failed to create Dataplex client %w", err) 84 | } 85 | return client, nil 86 | } 87 | 88 | func TestDataplexToolEndpoints(t *testing.T) { 89 | sourceConfig := getDataplexVars(t) 90 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 91 | defer cancel() 92 | 93 | var args []string 94 | 95 | bigqueryClient, err := initBigQueryConnection(ctx, DataplexProject) 96 | if err != nil { 97 | t.Fatalf("unable to create Cloud SQL connection pool: %s", err) 98 | } 99 | 100 | dataplexClient, err := initDataplexConnection(ctx) 101 | if err != nil { 102 | t.Fatalf("unable to create Dataplex connection: %s", err) 103 | } 104 | 105 | // create resources with UUID 106 | datasetName := fmt.Sprintf("temp_toolbox_test_%s", strings.ReplaceAll(uuid.New().String(), "-", "")) 107 | tableName := fmt.Sprintf("param_table_%s", strings.ReplaceAll(uuid.New().String(), "-", "")) 108 | aspectTypeId := fmt.Sprintf("param-aspect-type-%s", strings.ReplaceAll(uuid.New().String(), "-", "")) 109 | 110 | teardownTable1 := setupBigQueryTable(t, ctx, bigqueryClient, datasetName, tableName) 111 | teardownAspectType1 := setupDataplexThirdPartyAspectType(t, ctx, dataplexClient, aspectTypeId) 112 | time.Sleep(2 * time.Minute) // wait for table and aspect type to be ingested 113 | defer teardownTable1(t) 114 | defer teardownAspectType1(t) 115 | 116 | toolsFile := getDataplexToolsConfig(sourceConfig) 117 | 118 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 119 | if err != nil { 120 | t.Fatalf("command initialization returned an error: %s", err) 121 | } 122 | defer cleanup() 123 | 124 | waitCtx, cancel := context.WithTimeout(ctx, 3*time.Minute) 125 | defer cancel() 126 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 127 | if err != nil { 128 | t.Logf("toolbox command logs: \n%s", out) 129 | t.Fatalf("toolbox didn't start successfully: %s", err) 130 | } 131 | 132 | runDataplexToolGetTest(t) 133 | runDataplexSearchEntriesToolInvokeTest(t, tableName, datasetName) 134 | runDataplexLookupEntryToolInvokeTest(t, tableName, datasetName) 135 | runDataplexSearchAspectTypesToolInvokeTest(t, aspectTypeId) 136 | } 137 | 138 | func setupBigQueryTable(t *testing.T, ctx context.Context, client *bigqueryapi.Client, datasetName string, tableName string) func(*testing.T) { 139 | // Create dataset 140 | dataset := client.Dataset(datasetName) 141 | _, err := dataset.Metadata(ctx) 142 | 143 | if err != nil { 144 | apiErr, ok := err.(*googleapi.Error) 145 | if !ok || apiErr.Code != 404 { 146 | t.Fatalf("Failed to check dataset %q existence: %v", datasetName, err) 147 | } 148 | metadataToCreate := &bigqueryapi.DatasetMetadata{Name: datasetName} 149 | if err := dataset.Create(ctx, metadataToCreate); err != nil { 150 | t.Fatalf("Failed to create dataset %q: %v", datasetName, err) 151 | } 152 | } 153 | 154 | // Create table 155 | tab := client.Dataset(datasetName).Table(tableName) 156 | meta := &bigqueryapi.TableMetadata{} 157 | if err := tab.Create(ctx, meta); err != nil { 158 | t.Fatalf("Create table job for %s failed: %v", tableName, err) 159 | } 160 | 161 | return func(t *testing.T) { 162 | // tear down table 163 | dropSQL := fmt.Sprintf("drop table %s.%s", datasetName, tableName) 164 | dropJob, err := client.Query(dropSQL).Run(ctx) 165 | if err != nil { 166 | t.Errorf("Failed to start drop table job for %s: %v", tableName, err) 167 | return 168 | } 169 | dropStatus, err := dropJob.Wait(ctx) 170 | if err != nil { 171 | t.Errorf("Failed to wait for drop table job for %s: %v", tableName, err) 172 | return 173 | } 174 | if err := dropStatus.Err(); err != nil { 175 | t.Errorf("Error dropping table %s: %v", tableName, err) 176 | } 177 | 178 | // tear down dataset 179 | datasetToTeardown := client.Dataset(datasetName) 180 | tablesIterator := datasetToTeardown.Tables(ctx) 181 | _, err = tablesIterator.Next() 182 | 183 | if err == iterator.Done { 184 | if err := datasetToTeardown.Delete(ctx); err != nil { 185 | t.Errorf("Failed to delete dataset %s: %v", datasetName, err) 186 | } 187 | } else if err != nil { 188 | t.Errorf("Failed to list tables in dataset %s to check emptiness: %v.", datasetName, err) 189 | } 190 | } 191 | } 192 | 193 | func setupDataplexThirdPartyAspectType(t *testing.T, ctx context.Context, client *dataplex.CatalogClient, aspectTypeId string) func(*testing.T) { 194 | parent := fmt.Sprintf("projects/%s/locations/us", DataplexProject) 195 | createAspectTypeReq := &dataplexpb.CreateAspectTypeRequest{ 196 | Parent: parent, 197 | AspectTypeId: aspectTypeId, 198 | AspectType: &dataplexpb.AspectType{ 199 | Name: fmt.Sprintf("%s/aspectTypes/%s", parent, aspectTypeId), 200 | MetadataTemplate: &dataplexpb.AspectType_MetadataTemplate{ 201 | Name: "UserSchema", 202 | Type: "record", 203 | }, 204 | }, 205 | } 206 | _, err := client.CreateAspectType(ctx, createAspectTypeReq) 207 | if err != nil { 208 | t.Fatalf("Failed to create aspect type %s: %v", aspectTypeId, err) 209 | } 210 | 211 | return func(t *testing.T) { 212 | // tear down aspect type 213 | deleteAspectTypeReq := &dataplexpb.DeleteAspectTypeRequest{ 214 | Name: fmt.Sprintf("%s/aspectTypes/%s", parent, aspectTypeId), 215 | } 216 | if _, err := client.DeleteAspectType(ctx, deleteAspectTypeReq); err != nil { 217 | t.Errorf("Failed to delete aspect type %s: %v", aspectTypeId, err) 218 | } 219 | } 220 | } 221 | 222 | func getDataplexToolsConfig(sourceConfig map[string]any) map[string]any { 223 | // Write config into a file and pass it to command 224 | toolsFile := map[string]any{ 225 | "sources": map[string]any{ 226 | "my-dataplex-instance": sourceConfig, 227 | }, 228 | "authServices": map[string]any{ 229 | "my-google-auth": map[string]any{ 230 | "kind": "google", 231 | "clientId": tests.ClientId, 232 | }, 233 | }, 234 | "tools": map[string]any{ 235 | "my-dataplex-search-entries-tool": map[string]any{ 236 | "kind": DataplexSearchEntriesToolKind, 237 | "source": "my-dataplex-instance", 238 | "description": "Simple dataplex search entries tool to test end to end functionality.", 239 | }, 240 | "my-auth-dataplex-search-entries-tool": map[string]any{ 241 | "kind": DataplexSearchEntriesToolKind, 242 | "source": "my-dataplex-instance", 243 | "description": "Simple dataplex search entries tool to test end to end functionality.", 244 | "authRequired": []string{"my-google-auth"}, 245 | }, 246 | "my-dataplex-lookup-entry-tool": map[string]any{ 247 | "kind": DataplexLookupEntryToolKind, 248 | "source": "my-dataplex-instance", 249 | "description": "Simple dataplex lookup entry tool to test end to end functionality.", 250 | }, 251 | "my-auth-dataplex-lookup-entry-tool": map[string]any{ 252 | "kind": DataplexLookupEntryToolKind, 253 | "source": "my-dataplex-instance", 254 | "description": "Simple dataplex lookup entry tool to test end to end functionality.", 255 | "authRequired": []string{"my-google-auth"}, 256 | }, 257 | "my-dataplex-search-aspect-types-tool": map[string]any{ 258 | "kind": DataplexSearchAspectTypesToolKind, 259 | "source": "my-dataplex-instance", 260 | "description": "Simple dataplex search aspect types tool to test end to end functionality.", 261 | }, 262 | "my-auth-dataplex-search-aspect-types-tool": map[string]any{ 263 | "kind": DataplexSearchAspectTypesToolKind, 264 | "source": "my-dataplex-instance", 265 | "description": "Simple dataplex search aspect types tool to test end to end functionality.", 266 | "authRequired": []string{"my-google-auth"}, 267 | }, 268 | }, 269 | } 270 | 271 | return toolsFile 272 | } 273 | 274 | func runDataplexToolGetTest(t *testing.T) { 275 | testCases := []struct { 276 | name string 277 | toolName string 278 | expectedParams []string 279 | }{ 280 | { 281 | name: "get my-dataplex-search-entries-tool", 282 | toolName: "my-dataplex-search-entries-tool", 283 | expectedParams: []string{"pageSize", "query", "orderBy"}, 284 | }, 285 | { 286 | name: "get my-dataplex-lookup-entry-tool", 287 | toolName: "my-dataplex-lookup-entry-tool", 288 | expectedParams: []string{"name", "view", "aspectTypes", "entry"}, 289 | }, 290 | { 291 | name: "get my-dataplex-search-aspect-types-tool", 292 | toolName: "my-dataplex-search-aspect-types-tool", 293 | expectedParams: []string{"pageSize", "query", "orderBy"}, 294 | }, 295 | } 296 | 297 | for _, tc := range testCases { 298 | t.Run(tc.name, func(t *testing.T) { 299 | resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:5000/api/tool/%s/", tc.toolName)) 300 | if err != nil { 301 | t.Fatalf("error when sending a request: %s", err) 302 | } 303 | defer resp.Body.Close() 304 | if resp.StatusCode != 200 { 305 | t.Fatalf("response status code is not 200") 306 | } 307 | var body map[string]interface{} 308 | err = json.NewDecoder(resp.Body).Decode(&body) 309 | if err != nil { 310 | t.Fatalf("error parsing response body") 311 | } 312 | got, ok := body["tools"] 313 | if !ok { 314 | t.Fatalf("unable to find tools in response body") 315 | } 316 | 317 | toolsMap, ok := got.(map[string]interface{}) 318 | if !ok { 319 | t.Fatalf("expected 'tools' to be a map, got %T", got) 320 | } 321 | tool, ok := toolsMap[tc.toolName].(map[string]interface{}) 322 | if !ok { 323 | t.Fatalf("expected tool %q to be a map, got %T", tc.toolName, toolsMap[tc.toolName]) 324 | } 325 | params, ok := tool["parameters"].([]interface{}) 326 | if !ok { 327 | t.Fatalf("expected 'parameters' to be a slice, got %T", tool["parameters"]) 328 | } 329 | paramSet := make(map[string]struct{}) 330 | for _, param := range params { 331 | paramMap, ok := param.(map[string]interface{}) 332 | if ok { 333 | if name, ok := paramMap["name"].(string); ok { 334 | paramSet[name] = struct{}{} 335 | } 336 | } 337 | } 338 | var missing []string 339 | for _, want := range tc.expectedParams { 340 | if _, found := paramSet[want]; !found { 341 | missing = append(missing, want) 342 | } 343 | } 344 | if len(missing) > 0 { 345 | t.Fatalf("missing parameters for tool %q: %v", tc.toolName, missing) 346 | } 347 | }) 348 | } 349 | } 350 | 351 | func runDataplexSearchEntriesToolInvokeTest(t *testing.T, tableName string, datasetName string) { 352 | idToken, err := tests.GetGoogleIdToken(tests.ClientId) 353 | if err != nil { 354 | t.Fatalf("error getting Google ID token: %s", err) 355 | } 356 | 357 | testCases := []struct { 358 | name string 359 | api string 360 | requestHeader map[string]string 361 | requestBody io.Reader 362 | wantStatusCode int 363 | expectResult bool 364 | wantContentKey string 365 | }{ 366 | { 367 | name: "Success - Entry Found", 368 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-entries-tool/invoke", 369 | requestHeader: map[string]string{}, 370 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))), 371 | wantStatusCode: 200, 372 | expectResult: true, 373 | wantContentKey: "dataplex_entry", 374 | }, 375 | { 376 | name: "Success with Authorization - Entry Found", 377 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-entries-tool/invoke", 378 | requestHeader: map[string]string{"my-google-auth_token": idToken}, 379 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))), 380 | wantStatusCode: 200, 381 | expectResult: true, 382 | wantContentKey: "dataplex_entry", 383 | }, 384 | { 385 | name: "Failure - Invalid Authorization Token", 386 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-entries-tool/invoke", 387 | requestHeader: map[string]string{"my-google-auth_token": "invalid_token"}, 388 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))), 389 | wantStatusCode: 401, 390 | expectResult: false, 391 | wantContentKey: "dataplex_entry", 392 | }, 393 | { 394 | name: "Failure - Without Authorization Token", 395 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-entries-tool/invoke", 396 | requestHeader: map[string]string{}, 397 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"displayname=%s system=bigquery parent:%s\"}", tableName, datasetName))), 398 | wantStatusCode: 401, 399 | expectResult: false, 400 | wantContentKey: "dataplex_entry", 401 | }, 402 | { 403 | name: "Failure - Entry Not Found", 404 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-entries-tool/invoke", 405 | requestHeader: map[string]string{}, 406 | requestBody: bytes.NewBuffer([]byte(`{"query":"displayname=\"\" system=bigquery parent:\"\""}`)), 407 | wantStatusCode: 200, 408 | expectResult: false, 409 | wantContentKey: "", 410 | }, 411 | } 412 | 413 | for _, tc := range testCases { 414 | t.Run(tc.name, func(t *testing.T) { 415 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody) 416 | if err != nil { 417 | t.Fatalf("unable to create request: %s", err) 418 | } 419 | req.Header.Add("Content-type", "application/json") 420 | for k, v := range tc.requestHeader { 421 | req.Header.Add(k, v) 422 | } 423 | resp, err := http.DefaultClient.Do(req) 424 | if err != nil { 425 | t.Fatalf("unable to send request: %s", err) 426 | } 427 | defer resp.Body.Close() 428 | if resp.StatusCode != tc.wantStatusCode { 429 | t.Fatalf("response status code is not %d. It is %d", tc.wantStatusCode, resp.StatusCode) 430 | bodyBytes, _ := io.ReadAll(resp.Body) 431 | t.Fatalf("Response body: %s", string(bodyBytes)) 432 | } 433 | var result map[string]interface{} 434 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 435 | t.Fatalf("error parsing response body: %s", err) 436 | } 437 | resultStr, ok := result["result"].(string) 438 | if !ok { 439 | if result["result"] == nil && !tc.expectResult { 440 | return 441 | } 442 | t.Fatalf("expected 'result' field to be a string, got %T", result["result"]) 443 | } 444 | if !tc.expectResult && (resultStr == "" || resultStr == "[]") { 445 | return 446 | } 447 | var entries []interface{} 448 | if err := json.Unmarshal([]byte(resultStr), &entries); err != nil { 449 | t.Fatalf("error unmarshalling result string: %v", err) 450 | } 451 | 452 | if tc.expectResult { 453 | if len(entries) != 1 { 454 | t.Fatalf("expected exactly one entry, but got %d", len(entries)) 455 | } 456 | entry, ok := entries[0].(map[string]interface{}) 457 | if !ok { 458 | t.Fatalf("expected first entry to be a map, got %T", entries[0]) 459 | } 460 | if _, ok := entry[tc.wantContentKey]; !ok { 461 | t.Fatalf("expected entry to have key '%s', but it was not found in %v", tc.wantContentKey, entry) 462 | } 463 | } else { 464 | if len(entries) != 0 { 465 | t.Fatalf("expected 0 entries, but got %d", len(entries)) 466 | } 467 | } 468 | }) 469 | } 470 | } 471 | 472 | func runDataplexLookupEntryToolInvokeTest(t *testing.T, tableName string, datasetName string) { 473 | idToken, err := tests.GetGoogleIdToken(tests.ClientId) 474 | if err != nil { 475 | t.Fatalf("error getting Google ID token: %s", err) 476 | } 477 | 478 | testCases := []struct { 479 | name string 480 | wantStatusCode int 481 | api string 482 | requestHeader map[string]string 483 | requestBody io.Reader 484 | expectResult bool 485 | wantContentKey string 486 | dontWantContentKey string 487 | aspectCheck bool 488 | reqBodyMap map[string]any 489 | }{ 490 | { 491 | name: "Success - Entry Found", 492 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-lookup-entry-tool/invoke", 493 | requestHeader: map[string]string{}, 494 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s\"}", DataplexProject, DataplexProject, DataplexProject, datasetName))), 495 | wantStatusCode: 200, 496 | expectResult: true, 497 | wantContentKey: "name", 498 | }, 499 | { 500 | name: "Success - Entry Found with Authorization", 501 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-lookup-entry-tool/invoke", 502 | requestHeader: map[string]string{"my-google-auth_token": idToken}, 503 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s\"}", DataplexProject, DataplexProject, DataplexProject, datasetName))), 504 | wantStatusCode: 200, 505 | expectResult: true, 506 | wantContentKey: "name", 507 | }, 508 | { 509 | name: "Failure - Invalid Authorization Token", 510 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-lookup-entry-tool/invoke", 511 | requestHeader: map[string]string{"my-google-auth_token": "invalid_token"}, 512 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s\"}", DataplexProject, DataplexProject, DataplexProject, datasetName))), 513 | wantStatusCode: 401, 514 | expectResult: false, 515 | wantContentKey: "name", 516 | }, 517 | { 518 | name: "Failure - Without Authorization Token", 519 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-lookup-entry-tool/invoke", 520 | requestHeader: map[string]string{}, 521 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s\"}", DataplexProject, DataplexProject, DataplexProject, datasetName))), 522 | wantStatusCode: 401, 523 | expectResult: false, 524 | wantContentKey: "name", 525 | }, 526 | { 527 | name: "Failure - Entry Not Found or Permission Denied", 528 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-lookup-entry-tool/invoke", 529 | requestHeader: map[string]string{}, 530 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s\"}", DataplexProject, DataplexProject, DataplexProject, "non-existent-dataset"))), 531 | wantStatusCode: 400, 532 | expectResult: false, 533 | }, 534 | { 535 | name: "Success - Entry Found with Basic View", 536 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-lookup-entry-tool/invoke", 537 | requestHeader: map[string]string{}, 538 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s/tables/%s\", \"view\": %d}", DataplexProject, DataplexProject, DataplexProject, datasetName, tableName, 1))), 539 | wantStatusCode: 200, 540 | expectResult: true, 541 | wantContentKey: "name", 542 | dontWantContentKey: "aspects", 543 | }, 544 | { 545 | name: "Failure - Entry with Custom View without Aspect Types", 546 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-lookup-entry-tool/invoke", 547 | requestHeader: map[string]string{}, 548 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s/tables/%s\", \"view\": %d}", DataplexProject, DataplexProject, DataplexProject, datasetName, tableName, 3))), 549 | wantStatusCode: 400, 550 | expectResult: false, 551 | }, 552 | { 553 | name: "Success - Entry Found with only Schema Aspect", 554 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-lookup-entry-tool/invoke", 555 | requestHeader: map[string]string{}, 556 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"name\":\"projects/%s/locations/us\", \"entry\":\"projects/%s/locations/us/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/%s/datasets/%s/tables/%s\", \"aspectTypes\":[\"projects/dataplex-types/locations/global/aspectTypes/schema\"], \"view\": %d}", DataplexProject, DataplexProject, DataplexProject, datasetName, tableName, 3))), 557 | wantStatusCode: 200, 558 | expectResult: true, 559 | wantContentKey: "aspects", 560 | aspectCheck: true, 561 | }, 562 | } 563 | 564 | for _, tc := range testCases { 565 | t.Run(tc.name, func(t *testing.T) { 566 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody) 567 | if err != nil { 568 | t.Fatalf("unable to create request: %s", err) 569 | } 570 | req.Header.Add("Content-type", "application/json") 571 | for k, v := range tc.requestHeader { 572 | req.Header.Add(k, v) 573 | } 574 | resp, err := http.DefaultClient.Do(req) 575 | if err != nil { 576 | t.Fatalf("unable to send request: %s", err) 577 | } 578 | defer resp.Body.Close() 579 | 580 | if resp.StatusCode != tc.wantStatusCode { 581 | bodyBytes, _ := io.ReadAll(resp.Body) 582 | t.Fatalf("Response status code got %d, want %d\nResponse body: %s", resp.StatusCode, tc.wantStatusCode, string(bodyBytes)) 583 | } 584 | 585 | var result map[string]interface{} 586 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 587 | t.Fatalf("Error parsing response body: %v", err) 588 | } 589 | 590 | if tc.expectResult { 591 | resultStr, ok := result["result"].(string) 592 | if !ok { 593 | t.Fatalf("Expected 'result' field to be a string on success, got %T", result["result"]) 594 | } 595 | if resultStr == "" || resultStr == "{}" || resultStr == "null" { 596 | t.Fatal("Expected an entry, but got empty result") 597 | } 598 | 599 | var entry map[string]interface{} 600 | if err := json.Unmarshal([]byte(resultStr), &entry); err != nil { 601 | t.Fatalf("Error unmarshalling result string into entry map: %v", err) 602 | } 603 | 604 | if _, ok := entry[tc.wantContentKey]; !ok { 605 | t.Fatalf("Expected entry to have key '%s', but it was not found in %v", tc.wantContentKey, entry) 606 | } 607 | 608 | if _, ok := entry[tc.dontWantContentKey]; ok { 609 | t.Fatalf("Expected entry to not have key '%s', but it was found in %v", tc.dontWantContentKey, entry) 610 | } 611 | 612 | if tc.aspectCheck { 613 | // Check length of aspects 614 | aspects, ok := entry["aspects"].(map[string]interface{}) 615 | if !ok { 616 | t.Fatalf("Expected 'aspects' to be a map, got %T", aspects) 617 | } 618 | if len(aspects) != 1 { 619 | t.Fatalf("Expected exactly one aspect, but got %d", len(aspects)) 620 | } 621 | } 622 | } else { // Handle expected error response 623 | _, ok := result["error"] 624 | if !ok { 625 | t.Fatalf("Expected 'error' field in response, got %v", result) 626 | } 627 | } 628 | }) 629 | } 630 | } 631 | 632 | func runDataplexSearchAspectTypesToolInvokeTest(t *testing.T, aspectTypeId string) { 633 | idToken, err := tests.GetGoogleIdToken(tests.ClientId) 634 | if err != nil { 635 | t.Fatalf("error getting Google ID token: %s", err) 636 | } 637 | 638 | testCases := []struct { 639 | name string 640 | api string 641 | requestHeader map[string]string 642 | requestBody io.Reader 643 | wantStatusCode int 644 | expectResult bool 645 | wantContentKey string 646 | }{ 647 | { 648 | name: "Success - Aspect Type Found", 649 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-aspect-types-tool/invoke", 650 | requestHeader: map[string]string{}, 651 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))), 652 | wantStatusCode: 200, 653 | expectResult: true, 654 | wantContentKey: "metadata_template", 655 | }, 656 | { 657 | name: "Success - Aspect Type Found with Authorization", 658 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-aspect-types-tool/invoke", 659 | requestHeader: map[string]string{"my-google-auth_token": idToken}, 660 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))), 661 | wantStatusCode: 200, 662 | expectResult: true, 663 | wantContentKey: "metadata_template", 664 | }, 665 | { 666 | name: "Failure - Aspect Type Not Found", 667 | api: "http://127.0.0.1:5000/api/tool/my-dataplex-search-aspect-types-tool/invoke", 668 | requestHeader: map[string]string{}, 669 | requestBody: bytes.NewBuffer([]byte(`"{\"query\":\"name:_aspectType\"}"`)), 670 | wantStatusCode: 400, 671 | expectResult: false, 672 | }, 673 | { 674 | name: "Failure - Invalid Authorization Token", 675 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-aspect-types-tool/invoke", 676 | requestHeader: map[string]string{"my-google-auth_token": "invalid_token"}, 677 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))), 678 | wantStatusCode: 401, 679 | expectResult: false, 680 | }, 681 | { 682 | name: "Failure - No Authorization Token", 683 | api: "http://127.0.0.1:5000/api/tool/my-auth-dataplex-search-aspect-types-tool/invoke", 684 | requestHeader: map[string]string{}, 685 | requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"query\":\"name:%s_aspectType\"}", aspectTypeId))), 686 | wantStatusCode: 401, 687 | expectResult: false, 688 | }, 689 | } 690 | 691 | for _, tc := range testCases { 692 | t.Run(tc.name, func(t *testing.T) { 693 | req, err := http.NewRequest(http.MethodPost, tc.api, tc.requestBody) 694 | if err != nil { 695 | t.Fatalf("unable to create request: %s", err) 696 | } 697 | req.Header.Add("Content-type", "application/json") 698 | for k, v := range tc.requestHeader { 699 | req.Header.Add(k, v) 700 | } 701 | resp, err := http.DefaultClient.Do(req) 702 | if err != nil { 703 | t.Fatalf("unable to send request: %s", err) 704 | } 705 | defer resp.Body.Close() 706 | if resp.StatusCode != tc.wantStatusCode { 707 | t.Fatalf("response status code is not %d. It is %d", tc.wantStatusCode, resp.StatusCode) 708 | } 709 | var result map[string]interface{} 710 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 711 | t.Fatalf("error parsing response body: %s", err) 712 | } 713 | resultStr, ok := result["result"].(string) 714 | if !ok { 715 | if result["result"] == nil && !tc.expectResult { 716 | return 717 | } 718 | t.Fatalf("expected 'result' field to be a string, got %T", result["result"]) 719 | } 720 | if !tc.expectResult && (resultStr == "" || resultStr == "[]") { 721 | return 722 | } 723 | var entries []interface{} 724 | if err := json.Unmarshal([]byte(resultStr), &entries); err != nil { 725 | t.Fatalf("error unmarshalling result string: %v", err) 726 | } 727 | 728 | if tc.expectResult { 729 | if len(entries) != 1 { 730 | t.Fatalf("expected exactly one entry, but got %d", len(entries)) 731 | } 732 | entry, ok := entries[0].(map[string]interface{}) 733 | if !ok { 734 | t.Fatalf("expected entry to be a map, got %T", entries[0]) 735 | } 736 | if _, ok := entry[tc.wantContentKey]; !ok { 737 | t.Fatalf("expected entry to have key '%s', but it was not found in %v", tc.wantContentKey, entry) 738 | } 739 | } else { 740 | if len(entries) != 0 { 741 | t.Fatalf("expected 0 entries, but got %d", len(entries)) 742 | } 743 | } 744 | }) 745 | } 746 | } 747 | ``` -------------------------------------------------------------------------------- /tests/looker/looker_integration_test.go: -------------------------------------------------------------------------------- ```go 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package looker 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | "os" 24 | "regexp" 25 | "strings" 26 | "testing" 27 | "time" 28 | 29 | "github.com/googleapis/genai-toolbox/internal/log" 30 | "github.com/googleapis/genai-toolbox/internal/testutils" 31 | "github.com/googleapis/genai-toolbox/internal/util" 32 | "github.com/googleapis/genai-toolbox/tests" 33 | ) 34 | 35 | var ( 36 | LookerSourceKind = "looker" 37 | LookerBaseUrl = os.Getenv("LOOKER_BASE_URL") 38 | LookerVerifySsl = os.Getenv("LOOKER_VERIFY_SSL") 39 | LookerClientId = os.Getenv("LOOKER_CLIENT_ID") 40 | LookerClientSecret = os.Getenv("LOOKER_CLIENT_SECRET") 41 | LookerProject = os.Getenv("LOOKER_PROJECT") 42 | LookerLocation = os.Getenv("LOOKER_LOCATION") 43 | ) 44 | 45 | func getLookerVars(t *testing.T) map[string]any { 46 | switch "" { 47 | case LookerBaseUrl: 48 | t.Fatal("'LOOKER_BASE_URL' not set") 49 | case LookerVerifySsl: 50 | t.Fatal("'LOOKER_VERIFY_SSL' not set") 51 | case LookerClientId: 52 | t.Fatal("'LOOKER_CLIENT_ID' not set") 53 | case LookerClientSecret: 54 | t.Fatal("'LOOKER_CLIENT_SECRET' not set") 55 | case LookerProject: 56 | t.Fatal("'LOOKER_PROJECT' not set") 57 | case LookerLocation: 58 | t.Fatal("'LOOKER_LOCATION' not set") 59 | } 60 | 61 | return map[string]any{ 62 | "kind": LookerSourceKind, 63 | "base_url": LookerBaseUrl, 64 | "verify_ssl": (LookerVerifySsl == "true"), 65 | "client_id": LookerClientId, 66 | "client_secret": LookerClientSecret, 67 | "project": LookerProject, 68 | "location": LookerLocation, 69 | } 70 | } 71 | 72 | func TestLooker(t *testing.T) { 73 | sourceConfig := getLookerVars(t) 74 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 75 | defer cancel() 76 | 77 | testLogger, err := log.NewStdLogger(os.Stdout, os.Stderr, "info") 78 | if err != nil { 79 | t.Fatalf("unexpected error: %s", err) 80 | } 81 | ctx = util.WithLogger(ctx, testLogger) 82 | 83 | var args []string 84 | 85 | // Write config into a file and pass it to command 86 | 87 | toolsFile := map[string]any{ 88 | "sources": map[string]any{ 89 | "my-instance": sourceConfig, 90 | }, 91 | "tools": map[string]any{ 92 | "get_models": map[string]any{ 93 | "kind": "looker-get-models", 94 | "source": "my-instance", 95 | "description": "Simple tool to test end to end functionality.", 96 | }, 97 | "get_explores": map[string]any{ 98 | "kind": "looker-get-explores", 99 | "source": "my-instance", 100 | "description": "Simple tool to test end to end functionality.", 101 | }, 102 | "get_dimensions": map[string]any{ 103 | "kind": "looker-get-dimensions", 104 | "source": "my-instance", 105 | "description": "Simple tool to test end to end functionality.", 106 | }, 107 | "get_measures": map[string]any{ 108 | "kind": "looker-get-measures", 109 | "source": "my-instance", 110 | "description": "Simple tool to test end to end functionality.", 111 | }, 112 | "get_filters": map[string]any{ 113 | "kind": "looker-get-filters", 114 | "source": "my-instance", 115 | "description": "Simple tool to test end to end functionality.", 116 | }, 117 | "get_parameters": map[string]any{ 118 | "kind": "looker-get-parameters", 119 | "source": "my-instance", 120 | "description": "Simple tool to test end to end functionality.", 121 | }, 122 | "query": map[string]any{ 123 | "kind": "looker-query", 124 | "source": "my-instance", 125 | "description": "Simple tool to test end to end functionality.", 126 | }, 127 | "query_sql": map[string]any{ 128 | "kind": "looker-query-sql", 129 | "source": "my-instance", 130 | "description": "Simple tool to test end to end functionality.", 131 | }, 132 | "query_url": map[string]any{ 133 | "kind": "looker-query-url", 134 | "source": "my-instance", 135 | "description": "Simple tool to test end to end functionality.", 136 | }, 137 | "get_looks": map[string]any{ 138 | "kind": "looker-get-looks", 139 | "source": "my-instance", 140 | "description": "Simple tool to test end to end functionality.", 141 | }, 142 | "get_dashboards": map[string]any{ 143 | "kind": "looker-get-dashboards", 144 | "source": "my-instance", 145 | "description": "Simple tool to test end to end functionality.", 146 | }, 147 | "conversational_analytics": map[string]any{ 148 | "kind": "looker-conversational-analytics", 149 | "source": "my-instance", 150 | "description": "Simple tool to test end to end functionality.", 151 | }, 152 | "health_pulse": map[string]any{ 153 | "kind": "looker-health-pulse", 154 | "source": "my-instance", 155 | "description": "Checks the health of a Looker instance by running a series of checks on the system.", 156 | }, 157 | "health_analyze": map[string]any{ 158 | "kind": "looker-health-analyze", 159 | "source": "my-instance", 160 | "description": "Provides analysis of a Looker instance's projects, models, or explores.", 161 | }, 162 | "health_vacuum": map[string]any{ 163 | "kind": "looker-health-vacuum", 164 | "source": "my-instance", 165 | "description": "Vacuums unused content from a Looker instance.", 166 | }, 167 | }, 168 | } 169 | 170 | cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) 171 | if err != nil { 172 | t.Fatalf("command initialization returned an error: %s", err) 173 | } 174 | defer cleanup() 175 | 176 | waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 177 | defer cancel() 178 | out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out) 179 | if err != nil { 180 | t.Logf("toolbox command logs: \n%s", out) 181 | t.Fatalf("toolbox didn't start successfully: %s", err) 182 | } 183 | 184 | tests.RunToolGetTestByName(t, "get_models", 185 | map[string]any{ 186 | "get_models": map[string]any{ 187 | "description": "Simple tool to test end to end functionality.", 188 | "authRequired": []any{}, 189 | "parameters": []any{}, 190 | }, 191 | }, 192 | ) 193 | tests.RunToolGetTestByName(t, "get_explores", 194 | map[string]any{ 195 | "get_explores": map[string]any{ 196 | "description": "Simple tool to test end to end functionality.", 197 | "authRequired": []any{}, 198 | "parameters": []any{ 199 | map[string]any{ 200 | "authSources": []any{}, 201 | "description": "The model containing the explores.", 202 | "name": "model", 203 | "required": true, 204 | "type": "string", 205 | }, 206 | }, 207 | }, 208 | }, 209 | ) 210 | tests.RunToolGetTestByName(t, "get_dimensions", 211 | map[string]any{ 212 | "get_dimensions": map[string]any{ 213 | "description": "Simple tool to test end to end functionality.", 214 | "authRequired": []any{}, 215 | "parameters": []any{ 216 | map[string]any{ 217 | "authSources": []any{}, 218 | "description": "The model containing the explore.", 219 | "name": "model", 220 | "required": true, 221 | "type": "string", 222 | }, 223 | map[string]any{ 224 | "authSources": []any{}, 225 | "description": "The explore containing the fields.", 226 | "name": "explore", 227 | "required": true, 228 | "type": "string", 229 | }, 230 | }, 231 | }, 232 | }, 233 | ) 234 | tests.RunToolGetTestByName(t, "get_measures", 235 | map[string]any{ 236 | "get_measures": map[string]any{ 237 | "description": "Simple tool to test end to end functionality.", 238 | "authRequired": []any{}, 239 | "parameters": []any{ 240 | map[string]any{ 241 | "authSources": []any{}, 242 | "description": "The model containing the explore.", 243 | "name": "model", 244 | "required": true, 245 | "type": "string", 246 | }, 247 | map[string]any{ 248 | "authSources": []any{}, 249 | "description": "The explore containing the fields.", 250 | "name": "explore", 251 | "required": true, 252 | "type": "string", 253 | }, 254 | }, 255 | }, 256 | }, 257 | ) 258 | tests.RunToolGetTestByName(t, "get_parameters", 259 | map[string]any{ 260 | "get_parameters": map[string]any{ 261 | "description": "Simple tool to test end to end functionality.", 262 | "authRequired": []any{}, 263 | "parameters": []any{ 264 | map[string]any{ 265 | "authSources": []any{}, 266 | "description": "The model containing the explore.", 267 | "name": "model", 268 | "required": true, 269 | "type": "string", 270 | }, 271 | map[string]any{ 272 | "authSources": []any{}, 273 | "description": "The explore containing the fields.", 274 | "name": "explore", 275 | "required": true, 276 | "type": "string", 277 | }, 278 | }, 279 | }, 280 | }, 281 | ) 282 | tests.RunToolGetTestByName(t, "get_filters", 283 | map[string]any{ 284 | "get_filters": map[string]any{ 285 | "description": "Simple tool to test end to end functionality.", 286 | "authRequired": []any{}, 287 | "parameters": []any{ 288 | map[string]any{ 289 | "authSources": []any{}, 290 | "description": "The model containing the explore.", 291 | "name": "model", 292 | "required": true, 293 | "type": "string", 294 | }, 295 | map[string]any{ 296 | "authSources": []any{}, 297 | "description": "The explore containing the fields.", 298 | "name": "explore", 299 | "required": true, 300 | "type": "string", 301 | }, 302 | }, 303 | }, 304 | }, 305 | ) 306 | tests.RunToolGetTestByName(t, "query", 307 | map[string]any{ 308 | "query": map[string]any{ 309 | "description": "Simple tool to test end to end functionality.", 310 | "authRequired": []any{}, 311 | "parameters": []any{ 312 | map[string]any{ 313 | "authSources": []any{}, 314 | "description": "The model containing the explore.", 315 | "name": "model", 316 | "required": true, 317 | "type": "string", 318 | }, 319 | map[string]any{ 320 | "authSources": []any{}, 321 | "description": "The explore to be queried.", 322 | "name": "explore", 323 | "required": true, 324 | "type": "string", 325 | }, 326 | map[string]any{ 327 | "authSources": []any{}, 328 | "description": "The fields to be retrieved.", 329 | "items": map[string]any{ 330 | "authSources": []any{}, 331 | "description": "A field to be returned in the query", 332 | "name": "field", 333 | "required": true, 334 | "type": "string", 335 | }, 336 | "name": "fields", 337 | "required": true, 338 | "type": "array", 339 | }, 340 | map[string]any{ 341 | "additionalProperties": true, 342 | "authSources": []any{}, 343 | "description": "The filters for the query", 344 | "name": "filters", 345 | "required": false, 346 | "type": "object", 347 | }, 348 | map[string]any{ 349 | "authSources": []any{}, 350 | "description": "The query pivots (must be included in fields as well).", 351 | "items": map[string]any{ 352 | "authSources": []any{}, 353 | "description": "A field to be used as a pivot in the query", 354 | "name": "pivot_field", 355 | "required": false, 356 | "type": "string", 357 | }, 358 | "name": "pivots", 359 | "required": false, 360 | "type": "array", 361 | }, 362 | map[string]any{ 363 | "authSources": []any{}, 364 | "description": "The sorts like \"field.id desc 0\".", 365 | "items": map[string]any{ 366 | "authSources": []any{}, 367 | "description": "A field to be used as a sort in the query", 368 | "name": "sort_field", 369 | "required": false, 370 | "type": "string", 371 | }, 372 | "name": "sorts", 373 | "required": false, 374 | "type": "array", 375 | }, 376 | map[string]any{ 377 | "authSources": []any{}, 378 | "description": "The row limit.", 379 | "name": "limit", 380 | "required": false, 381 | "type": "integer", 382 | }, 383 | map[string]any{ 384 | "authSources": []any{}, 385 | "description": "The query timezone.", 386 | "name": "tz", 387 | "required": false, 388 | "type": "string", 389 | }, 390 | }, 391 | }, 392 | }, 393 | ) 394 | tests.RunToolGetTestByName(t, "query_sql", 395 | map[string]any{ 396 | "query_sql": map[string]any{ 397 | "description": "Simple tool to test end to end functionality.", 398 | "authRequired": []any{}, 399 | "parameters": []any{ 400 | map[string]any{ 401 | "authSources": []any{}, 402 | "description": "The model containing the explore.", 403 | "name": "model", 404 | "required": true, 405 | "type": "string", 406 | }, 407 | map[string]any{ 408 | "authSources": []any{}, 409 | "description": "The explore to be queried.", 410 | "name": "explore", 411 | "required": true, 412 | "type": "string", 413 | }, 414 | map[string]any{ 415 | "authSources": []any{}, 416 | "description": "The fields to be retrieved.", 417 | "items": map[string]any{ 418 | "authSources": []any{}, 419 | "description": "A field to be returned in the query", 420 | "name": "field", 421 | "required": true, 422 | "type": "string", 423 | }, 424 | "name": "fields", 425 | "required": true, 426 | "type": "array", 427 | }, 428 | map[string]any{ 429 | "additionalProperties": true, 430 | "authSources": []any{}, 431 | "description": "The filters for the query", 432 | "name": "filters", 433 | "required": false, 434 | "type": "object", 435 | }, 436 | map[string]any{ 437 | "authSources": []any{}, 438 | "description": "The query pivots (must be included in fields as well).", 439 | "items": map[string]any{ 440 | "authSources": []any{}, 441 | "description": "A field to be used as a pivot in the query", 442 | "name": "pivot_field", 443 | "required": false, 444 | "type": "string", 445 | }, 446 | "name": "pivots", 447 | "required": false, 448 | "type": "array", 449 | }, 450 | map[string]any{ 451 | "authSources": []any{}, 452 | "description": "The sorts like \"field.id desc 0\".", 453 | "items": map[string]any{ 454 | "authSources": []any{}, 455 | "description": "A field to be used as a sort in the query", 456 | "name": "sort_field", 457 | "required": false, 458 | "type": "string", 459 | }, 460 | "name": "sorts", 461 | "required": false, 462 | "type": "array", 463 | }, 464 | map[string]any{ 465 | "authSources": []any{}, 466 | "description": "The row limit.", 467 | "name": "limit", 468 | "required": false, 469 | "type": "integer", 470 | }, 471 | map[string]any{ 472 | "authSources": []any{}, 473 | "description": "The query timezone.", 474 | "name": "tz", 475 | "required": false, 476 | "type": "string", 477 | }, 478 | }, 479 | }, 480 | }, 481 | ) 482 | tests.RunToolGetTestByName(t, "query_url", 483 | map[string]any{ 484 | "query_url": map[string]any{ 485 | "description": "Simple tool to test end to end functionality.", 486 | "authRequired": []any{}, 487 | "parameters": []any{ 488 | map[string]any{ 489 | "authSources": []any{}, 490 | "description": "The model containing the explore.", 491 | "name": "model", 492 | "required": true, 493 | "type": "string", 494 | }, 495 | map[string]any{ 496 | "authSources": []any{}, 497 | "description": "The explore to be queried.", 498 | "name": "explore", 499 | "required": true, 500 | "type": "string", 501 | }, 502 | map[string]any{ 503 | "authSources": []any{}, 504 | "description": "The fields to be retrieved.", 505 | "items": map[string]any{ 506 | "authSources": []any{}, 507 | "description": "A field to be returned in the query", 508 | "name": "field", 509 | "required": true, 510 | "type": "string", 511 | }, 512 | "name": "fields", 513 | "required": true, 514 | "type": "array", 515 | }, 516 | map[string]any{ 517 | "additionalProperties": true, 518 | "authSources": []any{}, 519 | "description": "The filters for the query", 520 | "name": "filters", 521 | "required": false, 522 | "type": "object", 523 | }, 524 | map[string]any{ 525 | "authSources": []any{}, 526 | "description": "The query pivots (must be included in fields as well).", 527 | "items": map[string]any{ 528 | "authSources": []any{}, 529 | "description": "A field to be used as a pivot in the query", 530 | "name": "pivot_field", 531 | "required": false, 532 | "type": "string", 533 | }, 534 | "name": "pivots", 535 | "required": false, 536 | "type": "array", 537 | }, 538 | map[string]any{ 539 | "authSources": []any{}, 540 | "description": "The sorts like \"field.id desc 0\".", 541 | "items": map[string]any{ 542 | "authSources": []any{}, 543 | "description": "A field to be used as a sort in the query", 544 | "name": "sort_field", 545 | "required": false, 546 | "type": "string", 547 | }, 548 | "name": "sorts", 549 | "required": false, 550 | "type": "array", 551 | }, 552 | map[string]any{ 553 | "authSources": []any{}, 554 | "description": "The row limit.", 555 | "name": "limit", 556 | "required": false, 557 | "type": "integer", 558 | }, 559 | map[string]any{ 560 | "authSources": []any{}, 561 | "description": "The query timezone.", 562 | "name": "tz", 563 | "required": false, 564 | "type": "string", 565 | }, 566 | map[string]any{ 567 | "additionalProperties": true, 568 | "authSources": []any{}, 569 | "description": "The visualization config for the query", 570 | "name": "vis_config", 571 | "required": false, 572 | "type": "object", 573 | }, 574 | }, 575 | }, 576 | }, 577 | ) 578 | tests.RunToolGetTestByName(t, "get_looks", 579 | map[string]any{ 580 | "get_looks": map[string]any{ 581 | "description": "Simple tool to test end to end functionality.", 582 | "authRequired": []any{}, 583 | "parameters": []any{ 584 | map[string]any{ 585 | "authSources": []any{}, 586 | "description": "The title of the look.", 587 | "name": "title", 588 | "required": false, 589 | "type": "string", 590 | }, 591 | map[string]any{ 592 | "authSources": []any{}, 593 | "description": "The description of the look.", 594 | "name": "desc", 595 | "required": false, 596 | "type": "string", 597 | }, 598 | map[string]any{ 599 | "authSources": []any{}, 600 | "description": "The number of looks to fetch. Default 100", 601 | "name": "limit", 602 | "required": false, 603 | "type": "integer", 604 | }, 605 | map[string]any{ 606 | "authSources": []any{}, 607 | "description": "The number of looks to skip before fetching. Default 0", 608 | "name": "offset", 609 | "required": false, 610 | "type": "integer", 611 | }, 612 | }, 613 | }, 614 | }, 615 | ) 616 | tests.RunToolGetTestByName(t, "get_dashboards", 617 | map[string]any{ 618 | "get_dashboards": map[string]any{ 619 | "description": "Simple tool to test end to end functionality.", 620 | "authRequired": []any{}, 621 | "parameters": []any{ 622 | map[string]any{ 623 | "authSources": []any{}, 624 | "description": "The title of the dashboard.", 625 | "name": "title", 626 | "required": false, 627 | "type": "string", 628 | }, 629 | map[string]any{ 630 | "authSources": []any{}, 631 | "description": "The description of the dashboard.", 632 | "name": "desc", 633 | "required": false, 634 | "type": "string", 635 | }, 636 | map[string]any{ 637 | "authSources": []any{}, 638 | "description": "The number of dashboards to fetch. Default 100", 639 | "name": "limit", 640 | "required": false, 641 | "type": "integer", 642 | }, 643 | map[string]any{ 644 | "authSources": []any{}, 645 | "description": "The number of dashboards to skip before fetching. Default 0", 646 | "name": "offset", 647 | "required": false, 648 | "type": "integer", 649 | }, 650 | }, 651 | }, 652 | }, 653 | ) 654 | 655 | tests.RunToolGetTestByName(t, "conversational_analytics", 656 | map[string]any{ 657 | "conversational_analytics": map[string]any{ 658 | "description": "Simple tool to test end to end functionality.", 659 | "authRequired": []any{}, 660 | "parameters": []any{ 661 | map[string]any{ 662 | "authSources": []any{}, 663 | "description": "The user's question, potentially including conversation history and system instructions for context.", 664 | "name": "user_query_with_context", 665 | "required": true, 666 | "type": "string", 667 | }, 668 | map[string]any{ 669 | "authSources": []any{}, 670 | "description": "An Array of at least one and up to 5 explore references like [{'model': 'MODEL_NAME', 'explore': 'EXPLORE_NAME'}]", 671 | "items": map[string]any{ 672 | "additionalProperties": true, 673 | "authSources": []any{}, 674 | "name": "explore_reference", 675 | "description": "An explore reference like {'model': 'MODEL_NAME', 'explore': 'EXPLORE_NAME'}", 676 | "required": true, 677 | "type": "object", 678 | }, 679 | "name": "explore_references", 680 | "required": true, 681 | "type": "array", 682 | }, 683 | }, 684 | }, 685 | }, 686 | ) 687 | tests.RunToolGetTestByName(t, "health_pulse", 688 | map[string]any{ 689 | "health_pulse": map[string]any{ 690 | "description": "Checks the health of a Looker instance by running a series of checks on the system.", 691 | "authRequired": []any{}, 692 | "parameters": []any{ 693 | map[string]any{ 694 | "authSources": []any{}, 695 | "description": "The health check to run. Can be either: `check_db_connections`, `check_dashboard_performance`,`check_dashboard_errors`,`check_explore_performance`,`check_schedule_failures`, or `check_legacy_features`", 696 | "name": "action", 697 | "required": true, 698 | "type": "string", 699 | }, 700 | }, 701 | }, 702 | }, 703 | ) 704 | tests.RunToolGetTestByName(t, "health_analyze", 705 | map[string]any{ 706 | "health_analyze": map[string]any{ 707 | "description": "Provides analysis of a Looker instance's projects, models, or explores.", 708 | "authRequired": []any{}, 709 | "parameters": []any{ 710 | map[string]any{ 711 | "authSources": []any{}, 712 | "description": "The analysis to run. Can be 'projects', 'models', or 'explores'.", 713 | "name": "action", 714 | "required": true, 715 | "type": "string", 716 | }, 717 | map[string]any{ 718 | "authSources": []any{}, 719 | "description": "The Looker project to analyze (optional).", 720 | "name": "project", 721 | "required": false, 722 | "type": "string", 723 | }, 724 | map[string]any{ 725 | "authSources": []any{}, 726 | "description": "The Looker model to analyze (optional).", 727 | "name": "model", 728 | "required": false, 729 | "type": "string", 730 | }, 731 | map[string]any{ 732 | "authSources": []any{}, 733 | "description": "The Looker explore to analyze (optional).", 734 | "name": "explore", 735 | "required": false, 736 | "type": "string", 737 | }, 738 | map[string]any{ 739 | "authSources": []any{}, 740 | "description": "The timeframe in days to analyze.", 741 | "name": "timeframe", 742 | "required": false, 743 | "type": "integer", 744 | }, 745 | map[string]any{ 746 | "authSources": []any{}, 747 | "description": "The minimum number of queries for a model or explore to be considered used.", 748 | "name": "min_queries", 749 | "required": false, 750 | "type": "integer", 751 | }, 752 | }, 753 | }, 754 | }, 755 | ) 756 | tests.RunToolGetTestByName(t, "health_vacuum", 757 | map[string]any{ 758 | "health_vacuum": map[string]any{ 759 | "description": "Vacuums unused content from a Looker instance.", 760 | "authRequired": []any{}, 761 | "parameters": []any{ 762 | map[string]any{ 763 | "authSources": []any{}, 764 | "description": "The vacuum action to run. Can be 'models', or 'explores'.", 765 | "name": "action", 766 | "required": true, 767 | "type": "string", 768 | }, 769 | map[string]any{ 770 | "authSources": []any{}, 771 | "description": "The Looker project to vacuum (optional).", 772 | "name": "project", 773 | "required": false, 774 | "type": "string", 775 | }, 776 | map[string]any{ 777 | "authSources": []any{}, 778 | "description": "The Looker model to vacuum (optional).", 779 | "name": "model", 780 | "required": false, 781 | "type": "string", 782 | }, 783 | map[string]any{ 784 | "authSources": []any{}, 785 | "description": "The Looker explore to vacuum (optional).", 786 | "name": "explore", 787 | "required": false, 788 | "type": "string", 789 | }, 790 | map[string]any{ 791 | "authSources": []any{}, 792 | "description": "The timeframe in days to analyze.", 793 | "name": "timeframe", 794 | "required": false, 795 | "type": "integer", 796 | }, 797 | map[string]any{ 798 | "authSources": []any{}, 799 | "description": "The minimum number of queries for a model or explore to be considered used.", 800 | "name": "min_queries", 801 | "required": false, 802 | "type": "integer", 803 | }, 804 | }, 805 | }, 806 | }, 807 | ) 808 | 809 | wantResult := "{\"label\":\"System Activity\",\"name\":\"system__activity\",\"project_name\":\"system__activity\"}" 810 | tests.RunToolInvokeSimpleTest(t, "get_models", wantResult) 811 | 812 | wantResult = "{\"description\":\"Data about Look and dashboard usage, including frequency of views, favoriting, scheduling, embedding, and access via the API. Also includes details about individual Looks and dashboards.\",\"group_label\":\"System Activity\",\"label\":\"Content Usage\",\"name\":\"content_usage\"}" 813 | tests.RunToolInvokeParametersTest(t, "get_explores", []byte(`{"model": "system__activity"}`), wantResult) 814 | 815 | wantResult = "{\"description\":\"Number of times this content has been viewed via the Looker API\",\"label\":\"Content Usage API Count\",\"label_short\":\"API Count\",\"name\":\"content_usage.api_count\",\"type\":\"number\"}" 816 | tests.RunToolInvokeParametersTest(t, "get_dimensions", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult) 817 | 818 | wantResult = "{\"description\":\"The total number of views via the Looker API\",\"label\":\"Content Usage API Total\",\"label_short\":\"API Total\",\"name\":\"content_usage.api_total\",\"type\":\"sum\"}" 819 | tests.RunToolInvokeParametersTest(t, "get_measures", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult) 820 | 821 | wantResult = "[]" 822 | tests.RunToolInvokeParametersTest(t, "get_filters", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult) 823 | 824 | wantResult = "[]" 825 | tests.RunToolInvokeParametersTest(t, "get_parameters", []byte(`{"model": "system__activity", "explore": "content_usage"}`), wantResult) 826 | 827 | wantResult = "{\"look.count\":" 828 | tests.RunToolInvokeParametersTest(t, "query", []byte(`{"model": "system__activity", "explore": "look", "fields": ["look.count"]}`), wantResult) 829 | 830 | wantResult = "SELECT" 831 | tests.RunToolInvokeParametersTest(t, "query_sql", []byte(`{"model": "system__activity", "explore": "look", "fields": ["look.count"]}`), wantResult) 832 | 833 | wantResult = "system__activity" 834 | tests.RunToolInvokeParametersTest(t, "query_url", []byte(`{"model": "system__activity", "explore": "look", "fields": ["look.count"]}`), wantResult) 835 | 836 | // A system that is just being used for testing has no looks or dashboards 837 | wantResult = "null" 838 | tests.RunToolInvokeParametersTest(t, "get_looks", []byte(`{"title": "FOO", "desc": "BAR"}`), wantResult) 839 | 840 | wantResult = "null" 841 | tests.RunToolInvokeParametersTest(t, "get_dashboards", []byte(`{"title": "FOO", "desc": "BAR"}`), wantResult) 842 | 843 | runConversationalAnalytics(t, "system__activity", "content_usage") 844 | 845 | wantResult = "\"Connection\":\"thelook\"" 846 | tests.RunToolInvokeParametersTest(t, "health_pulse", []byte(`{"action": "check_db_connections"}`), wantResult) 847 | 848 | wantResult = "[]" 849 | tests.RunToolInvokeParametersTest(t, "health_pulse", []byte(`{"action": "check_schedule_failures"}`), wantResult) 850 | 851 | wantResult = "[{\"Feature\":\"Unsupported in Looker (Google Cloud core)\"}]" 852 | tests.RunToolInvokeParametersTest(t, "health_pulse", []byte(`{"action": "check_legacy_features"}`), wantResult) 853 | 854 | wantResult = "\"Project\":\"the_look\"" 855 | tests.RunToolInvokeParametersTest(t, "health_analyze", []byte(`{"action": "projects"}`), wantResult) 856 | 857 | wantResult = "\"Model\":\"the_look\"" 858 | tests.RunToolInvokeParametersTest(t, "health_analyze", []byte(`{"action": "explores", "project": "the_look", "model": "the_look", "explore": "inventory_items"}`), wantResult) 859 | 860 | wantResult = "\"Model\":\"the_look\"" 861 | tests.RunToolInvokeParametersTest(t, "health_vacuum", []byte(`{"action": "models"}`), wantResult) 862 | } 863 | 864 | func runConversationalAnalytics(t *testing.T, modelName, exploreName string) { 865 | exploreRefsJSON := fmt.Sprintf(`[{"model":"%s","explore":"%s"}]`, modelName, exploreName) 866 | 867 | var refs []map[string]any 868 | if err := json.Unmarshal([]byte(exploreRefsJSON), &refs); err != nil { 869 | t.Fatalf("failed to unmarshal explore refs: %v", err) 870 | } 871 | 872 | testCases := []struct { 873 | name string 874 | exploreRefs []map[string]any 875 | wantStatusCode int 876 | wantInResult string 877 | wantInError string 878 | }{ 879 | { 880 | name: "invoke conversational analytics with explore", 881 | exploreRefs: refs, 882 | wantStatusCode: http.StatusOK, 883 | wantInResult: `Answer`, 884 | }, 885 | } 886 | 887 | for _, tc := range testCases { 888 | t.Run(tc.name, func(t *testing.T) { 889 | requestBodyMap := map[string]any{ 890 | "user_query_with_context": "What is in the explore?", 891 | "explore_references": tc.exploreRefs, 892 | } 893 | bodyBytes, err := json.Marshal(requestBodyMap) 894 | if err != nil { 895 | t.Fatalf("failed to marshal request body: %v", err) 896 | } 897 | url := "http://127.0.0.1:5000/api/tool/conversational_analytics/invoke" 898 | resp, bodyBytes := tests.RunRequest(t, http.MethodPost, url, bytes.NewBuffer(bodyBytes), nil) 899 | 900 | if resp.StatusCode != tc.wantStatusCode { 901 | t.Fatalf("unexpected status code: got %d, want %d. Body: %s", resp.StatusCode, tc.wantStatusCode, string(bodyBytes)) 902 | } 903 | 904 | if tc.wantInResult != "" { 905 | var respBody map[string]interface{} 906 | if err := json.Unmarshal(bodyBytes, &respBody); err != nil { 907 | t.Fatalf("error parsing response body: %v", err) 908 | } 909 | got, ok := respBody["result"].(string) 910 | if !ok { 911 | t.Fatalf("unable to find result in response body") 912 | } 913 | if !strings.Contains(got, tc.wantInResult) { 914 | t.Errorf("unexpected result: got %q, want to contain %q", got, tc.wantInResult) 915 | } 916 | } 917 | 918 | if tc.wantInError != "" { 919 | if !strings.Contains(string(bodyBytes), tc.wantInError) { 920 | t.Errorf("unexpected error message: got %q, want to contain %q", string(bodyBytes), tc.wantInError) 921 | } 922 | } 923 | }) 924 | } 925 | } 926 | ``` -------------------------------------------------------------------------------- /internal/prebuiltconfigs/tools/looker.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | sources: 16 | looker-source: 17 | kind: looker 18 | base_url: ${LOOKER_BASE_URL} 19 | client_id: ${LOOKER_CLIENT_ID:} 20 | client_secret: ${LOOKER_CLIENT_SECRET:} 21 | verify_ssl: ${LOOKER_VERIFY_SSL:true} 22 | timeout: 600s 23 | use_client_oauth: ${LOOKER_USE_CLIENT_OAUTH:false} 24 | show_hidden_models: ${LOOKER_SHOW_HIDDEN_MODELS:true} 25 | show_hidden_explores: ${LOOKER_SHOW_HIDDEN_EXPLORES:true} 26 | show_hidden_fields: ${LOOKER_SHOW_HIDDEN_FIELDS:true} 27 | 28 | tools: 29 | get_models: 30 | kind: looker-get-models 31 | source: looker-source 32 | description: | 33 | The get_models tool retrieves the list of LookML models in the Looker system. 34 | 35 | It takes no parameters. 36 | 37 | get_explores: 38 | kind: looker-get-explores 39 | source: looker-source 40 | description: | 41 | The get_explores tool retrieves the list of explores defined in a LookML model 42 | in the Looker system. 43 | 44 | It takes one parameter, the model_name looked up from get_models. 45 | 46 | get_dimensions: 47 | kind: looker-get-dimensions 48 | source: looker-source 49 | description: | 50 | The get_dimensions tool retrieves the list of dimensions defined in 51 | an explore. 52 | 53 | It takes two parameters, the model_name looked up from get_models and the 54 | explore_name looked up from get_explores. 55 | 56 | If this returns a suggestions field for a dimension, the contents of suggestions 57 | can be used as filters for this field. If this returns a suggest_explore and 58 | suggest_dimension, a query against that explore and dimension can be used to find 59 | valid filters for this field. 60 | 61 | get_measures: 62 | kind: looker-get-measures 63 | source: looker-source 64 | description: | 65 | The get_measures tool retrieves the list of measures defined in 66 | an explore. 67 | 68 | It takes two parameters, the model_name looked up from get_models and the 69 | explore_name looked up from get_explores. 70 | 71 | If this returns a suggestions field for a measure, the contents of suggestions 72 | can be used as filters for this field. If this returns a suggest_explore and 73 | suggest_dimension, a query against that explore and dimension can be used to find 74 | valid filters for this field. 75 | 76 | get_filters: 77 | kind: looker-get-filters 78 | source: looker-source 79 | description: | 80 | The get_filters tool retrieves the list of filters defined in 81 | an explore. 82 | 83 | It takes two parameters, the model_name looked up from get_models and the 84 | explore_name looked up from get_explores. 85 | 86 | get_parameters: 87 | kind: looker-get-parameters 88 | source: looker-source 89 | description: | 90 | The get_parameters tool retrieves the list of parameters defined in 91 | an explore. 92 | 93 | It takes two parameters, the model_name looked up from get_models and the 94 | explore_name looked up from get_explores. 95 | 96 | query: 97 | kind: looker-query 98 | source: looker-source 99 | description: | 100 | Query Tool 101 | 102 | This tool is used to run a query against the LookML model. The 103 | model, explore, and fields list must be specified. Pivots, 104 | filters and sorts are optional. 105 | 106 | The model can be found from the get_models tool. The explore 107 | can be found from the get_explores tool passing in the model. 108 | The fields can be found from the get_dimensions, get_measures, 109 | get_filters, and get_parameters tools, passing in the model 110 | and the explore. 111 | 112 | Provide a model_id and explore_name, then a list 113 | of fields. Optionally a list of pivots can be provided. 114 | The pivots must also be included in the fields list. 115 | 116 | Filters are provided as a map of {"field.id": "condition", 117 | "field.id2": "condition2", ...}. Do not put the field.id in 118 | quotes. Filter expressions can be found at 119 | https://cloud.google.com/looker/docs/filter-expressions. There 120 | is one mistake in that, however, Use `not null` instead of `-NULL`. 121 | 122 | Sorts can be specified like [ "field.id desc 0" ]. 123 | 124 | An optional row limit can be added. If not provided the limit 125 | will default to 500. "-1" can be specified for unlimited. 126 | 127 | An optional query timezone can be added. The query_timezone to 128 | will default to that of the workstation where this MCP server 129 | is running, or Etc/UTC if that can't be determined. Not all 130 | models support custom timezones. 131 | 132 | The result of the query tool is JSON 133 | 134 | query_sql: 135 | kind: looker-query-sql 136 | source: looker-source 137 | description: | 138 | Query SQL Tool 139 | 140 | This tool is used to generate the SQL that Looker would 141 | run against the underlying database. The parameters are 142 | the same as the query tool. 143 | 144 | The result of the query sql tool is SQL text. 145 | 146 | query_url: 147 | kind: looker-query-url 148 | source: looker-source 149 | description: | 150 | Query URL Tool 151 | 152 | This tool is used to generate the URL of a query in Looker. 153 | The user can then explore the query further inside Looker. 154 | The tool also returns the query_id and slug. The parameters 155 | are the same as the query tool with an additional vis_config 156 | parameter. 157 | 158 | The vis_config is optional. If provided, it will be used to 159 | control the default visualization for the query. Here are 160 | some notes on making visualizations. 161 | 162 | ### Cartesian Charts (Area, Bar, Column, Line, Scatter) 163 | 164 | These chart types share a large number of configuration options. 165 | 166 | **General** 167 | * `type`: The type of visualization (`looker_area`, `looker_bar`, `looker_column`, `looker_line`, `looker_scatter`). 168 | * `series_types`: Override the chart type for individual series. 169 | * `show_view_names`: Display view names in labels and tooltips (`true`/`false`). 170 | * `series_labels`: Provide custom names for series. 171 | 172 | **Styling & Colors** 173 | * `colors`: An array of color values to be used for the chart series. 174 | * `series_colors`: A mapping of series names to specific color values. 175 | * `color_application`: Advanced controls for color palette application (collection, palette, reverse, etc.). 176 | * `font_size`: Font size for labels (e.g., '12px'). 177 | 178 | **Legend** 179 | * `hide_legend`: Show or hide the chart legend (`true`/`false`). 180 | * `legend_position`: Placement of the legend (`'center'`, `'left'`, `'right'`). 181 | 182 | **Axes** 183 | * `swap_axes`: Swap the X and Y axes (`true`/`false`). 184 | * `x_axis_scale`: Scale of the x-axis (`'auto'`, `'ordinal'`, `'linear'`, `'time'`). 185 | * `x_axis_reversed`, `y_axis_reversed`: Reverse the direction of an axis (`true`/`false`). 186 | * `x_axis_gridlines`, `y_axis_gridlines`: Display gridlines for an axis (`true`/`false`). 187 | * `show_x_axis_label`, `show_y_axis_label`: Show or hide the axis title (`true`/`false`). 188 | * `show_x_axis_ticks`, `show_y_axis_ticks`: Show or hide axis tick marks (`true`/`false`). 189 | * `x_axis_label`, `y_axis_label`: Set a custom title for an axis. 190 | * `x_axis_datetime_label`: A format string for datetime labels on the x-axis (e.g., `'%Y-%m'`). 191 | * `x_padding_left`, `x_padding_right`: Adjust padding on the ends of the x-axis. 192 | * `x_axis_label_rotation`, `x_axis_label_rotation_bar`: Set rotation for x-axis labels. 193 | * `x_axis_zoom`, `y_axis_zoom`: Enable zooming on an axis (`true`/`false`). 194 | * `y_axes`: An array of configuration objects for multiple y-axes. 195 | 196 | **Data & Series** 197 | * `stacking`: How to stack series (`''` for none, `'normal'`, `'percent'`). 198 | * `ordering`: Order of series in a stack (`'none'`, etc.). 199 | * `limit_displayed_rows`: Enable or disable limiting the number of rows displayed (`true`/`false`). 200 | * `limit_displayed_rows_values`: Configuration for the row limit (e.g., `{ "first_last": "first", "show_hide": "show", "num_rows": 10 }`). 201 | * `discontinuous_nulls`: How to render null values in line charts (`true`/`false`). 202 | * `point_style`: Style for points on line and area charts (`'none'`, `'circle'`, `'circle_outline'`). 203 | * `series_point_styles`: Override point styles for individual series. 204 | * `interpolation`: Line interpolation style (`'linear'`, `'monotone'`, `'step'`, etc.). 205 | * `show_value_labels`: Display values on data points (`true`/`false`). 206 | * `label_value_format`: A format string for value labels. 207 | * `show_totals_labels`: Display total labels on stacked charts (`true`/`false`). 208 | * `totals_color`: Color for total labels. 209 | * `show_silhouette`: Display a "silhouette" of hidden series in stacked charts (`true`/`false`). 210 | * `hidden_series`: An array of series names to hide from the visualization. 211 | 212 | **Scatter/Bubble Specific** 213 | * `size_by_field`: The field used to determine the size of bubbles. 214 | * `color_by_field`: The field used to determine the color of bubbles. 215 | * `plot_size_by_field`: Whether to display the size-by field in the legend. 216 | * `cluster_points`: Group nearby points into clusters (`true`/`false`). 217 | * `quadrants_enabled`: Display quadrants on the chart (`true`/`false`). 218 | * `quadrant_properties`: Configuration for quadrant labels and colors. 219 | * `custom_quadrant_value_x`, `custom_quadrant_value_y`: Set quadrant boundaries as a percentage. 220 | * `custom_quadrant_point_x`, `custom_quadrant_point_y`: Set quadrant boundaries to a specific value. 221 | 222 | **Miscellaneous** 223 | * `reference_lines`: Configuration for displaying reference lines. 224 | * `trend_lines`: Configuration for displaying trend lines. 225 | * `trellis`: Configuration for creating trellis (small multiple) charts. 226 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering interactions. 227 | 228 | ### Boxplot 229 | 230 | * Inherits most of the Cartesian chart options. 231 | * `type`: Must be `looker_boxplot`. 232 | 233 | ### Funnel 234 | 235 | * `type`: Must be `looker_funnel`. 236 | * `orientation`: How data is read (`'automatic'`, `'dataInRows'`, `'dataInColumns'`). 237 | * `percentType`: How percentages are calculated (`'percentOfMaxValue'`, `'percentOfPriorRow'`). 238 | * `labelPosition`, `valuePosition`, `percentPosition`: Placement of labels (`'left'`, `'right'`, `'inline'`, `'hidden'`). 239 | * `labelColor`, `labelColorEnabled`: Set a custom color for labels. 240 | * `labelOverlap`: Allow labels to overlap (`true`/`false`). 241 | * `barColors`: An array of colors for the funnel steps. 242 | * `color_application`: Advanced color palette controls. 243 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering. 244 | 245 | ### Pie / Donut 246 | 247 | * Pie charts must have exactly one dimension and one numerical measure. 248 | * `type`: Must be `looker_pie`. 249 | * `value_labels`: Where to display values (`'legend'`, `'labels'`). 250 | * `label_type`: The format of data labels (`'labPer'`, `'labVal'`, `'lab'`, `'val'`, `'per'`). 251 | * `start_angle`, `end_angle`: The start and end angles of the pie chart. 252 | * `inner_radius`: The inner radius, used to create a donut chart. 253 | * `series_colors`, `series_labels`: Override colors and labels for specific slices. 254 | * `color_application`: Advanced color palette controls. 255 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering. 256 | * `advanced_vis_config`: A string containing JSON for advanced Highcharts configuration. 257 | 258 | ### Waterfall 259 | 260 | * Inherits most of the Cartesian chart options. 261 | * `type`: Must be `looker_waterfall`. 262 | * `up_color`: Color for positive (increasing) values. 263 | * `down_color`: Color for negative (decreasing) values. 264 | * `total_color`: Color for the total bar. 265 | 266 | ### Word Cloud 267 | 268 | * `type`: Must be `looker_wordcloud`. 269 | * `rotation`: Enable random word rotation (`true`/`false`). 270 | * `colors`: An array of colors for the words. 271 | * `color_application`: Advanced color palette controls. 272 | * `crossfilterEnabled`, `crossfilters`: Configuration for cross-filtering. 273 | 274 | These are some sample vis_config settings. 275 | 276 | A bar chart - 277 | {{ 278 | "defaults_version": 1, 279 | "label_density": 25, 280 | "legend_position": "center", 281 | "limit_displayed_rows": false, 282 | "ordering": "none", 283 | "plot_size_by_field": false, 284 | "point_style": "none", 285 | "show_null_labels": false, 286 | "show_silhouette": false, 287 | "show_totals_labels": false, 288 | "show_value_labels": false, 289 | "show_view_names": false, 290 | "show_x_axis_label": true, 291 | "show_x_axis_ticks": true, 292 | "show_y_axis_labels": true, 293 | "show_y_axis_ticks": true, 294 | "stacking": "normal", 295 | "totals_color": "#808080", 296 | "trellis": "", 297 | "type": "looker_bar", 298 | "x_axis_gridlines": false, 299 | "x_axis_reversed": false, 300 | "x_axis_scale": "auto", 301 | "x_axis_zoom": true, 302 | "y_axis_combined": true, 303 | "y_axis_gridlines": true, 304 | "y_axis_reversed": false, 305 | "y_axis_scale_mode": "linear", 306 | "y_axis_tick_density": "default", 307 | "y_axis_tick_density_custom": 5, 308 | "y_axis_zoom": true 309 | }} 310 | 311 | A column chart with an option advanced_vis_config - 312 | {{ 313 | "advanced_vis_config": "{ chart: { type: 'pie', spacingBottom: 50, spacingLeft: 50, spacingRight: 50, spacingTop: 50, }, legend: { enabled: false, }, plotOptions: { pie: { dataLabels: { enabled: true, format: '\u003cb\u003e{key}\u003c/b\u003e\u003cspan style=\"font-weight: normal\"\u003e - {percentage:.2f}%\u003c/span\u003e', }, showInLegend: false, }, }, series: [], }", 314 | "colors": [ 315 | "grey" 316 | ], 317 | "defaults_version": 1, 318 | "hidden_fields": [], 319 | "label_density": 25, 320 | "legend_position": "center", 321 | "limit_displayed_rows": false, 322 | "note_display": "below", 323 | "note_state": "collapsed", 324 | "note_text": "Unsold inventory only", 325 | "ordering": "none", 326 | "plot_size_by_field": false, 327 | "point_style": "none", 328 | "series_colors": {}, 329 | "show_null_labels": false, 330 | "show_silhouette": false, 331 | "show_totals_labels": false, 332 | "show_value_labels": true, 333 | "show_view_names": false, 334 | "show_x_axis_label": true, 335 | "show_x_axis_ticks": true, 336 | "show_y_axis_labels": true, 337 | "show_y_axis_ticks": true, 338 | "stacking": "normal", 339 | "totals_color": "#808080", 340 | "trellis": "", 341 | "type": "looker_column", 342 | "x_axis_gridlines": false, 343 | "x_axis_reversed": false, 344 | "x_axis_scale": "auto", 345 | "x_axis_zoom": true, 346 | "y_axes": [], 347 | "y_axis_combined": true, 348 | "y_axis_gridlines": true, 349 | "y_axis_reversed": false, 350 | "y_axis_scale_mode": "linear", 351 | "y_axis_tick_density": "default", 352 | "y_axis_tick_density_custom": 5, 353 | "y_axis_zoom": true 354 | }} 355 | 356 | A line chart - 357 | {{ 358 | "defaults_version": 1, 359 | "hidden_pivots": {}, 360 | "hidden_series": [], 361 | "interpolation": "linear", 362 | "label_density": 25, 363 | "legend_position": "center", 364 | "limit_displayed_rows": false, 365 | "plot_size_by_field": false, 366 | "point_style": "none", 367 | "series_types": {}, 368 | "show_null_points": true, 369 | "show_value_labels": false, 370 | "show_view_names": false, 371 | "show_x_axis_label": true, 372 | "show_x_axis_ticks": true, 373 | "show_y_axis_labels": true, 374 | "show_y_axis_ticks": true, 375 | "stacking": "", 376 | "trellis": "", 377 | "type": "looker_line", 378 | "x_axis_gridlines": false, 379 | "x_axis_reversed": false, 380 | "x_axis_scale": "auto", 381 | "y_axis_combined": true, 382 | "y_axis_gridlines": true, 383 | "y_axis_reversed": false, 384 | "y_axis_scale_mode": "linear", 385 | "y_axis_tick_density": "default", 386 | "y_axis_tick_density_custom": 5 387 | }} 388 | 389 | An area chart - 390 | {{ 391 | "defaults_version": 1, 392 | "interpolation": "linear", 393 | "label_density": 25, 394 | "legend_position": "center", 395 | "limit_displayed_rows": false, 396 | "plot_size_by_field": false, 397 | "point_style": "none", 398 | "series_types": {}, 399 | "show_null_points": true, 400 | "show_silhouette": false, 401 | "show_totals_labels": false, 402 | "show_value_labels": false, 403 | "show_view_names": false, 404 | "show_x_axis_label": true, 405 | "show_x_axis_ticks": true, 406 | "show_y_axis_labels": true, 407 | "show_y_axis_ticks": true, 408 | "stacking": "normal", 409 | "totals_color": "#808080", 410 | "trellis": "", 411 | "type": "looker_area", 412 | "x_axis_gridlines": false, 413 | "x_axis_reversed": false, 414 | "x_axis_scale": "auto", 415 | "x_axis_zoom": true, 416 | "y_axis_combined": true, 417 | "y_axis_gridlines": true, 418 | "y_axis_reversed": false, 419 | "y_axis_scale_mode": "linear", 420 | "y_axis_tick_density": "default", 421 | "y_axis_tick_density_custom": 5, 422 | "y_axis_zoom": true 423 | }} 424 | 425 | A scatter plot - 426 | {{ 427 | "cluster_points": false, 428 | "custom_quadrant_point_x": 5, 429 | "custom_quadrant_point_y": 5, 430 | "custom_value_label_column": "", 431 | "custom_x_column": "", 432 | "custom_y_column": "", 433 | "defaults_version": 1, 434 | "hidden_fields": [], 435 | "hidden_pivots": {}, 436 | "hidden_points_if_no": [], 437 | "hidden_series": [], 438 | "interpolation": "linear", 439 | "label_density": 25, 440 | "legend_position": "center", 441 | "limit_displayed_rows": false, 442 | "limit_displayed_rows_values": { 443 | "first_last": "first", 444 | "num_rows": 0, 445 | "show_hide": "hide" 446 | }, 447 | "plot_size_by_field": false, 448 | "point_style": "circle", 449 | "quadrant_properties": { 450 | "0": { 451 | "color": "", 452 | "label": "Quadrant 1" 453 | }, 454 | "1": { 455 | "color": "", 456 | "label": "Quadrant 2" 457 | }, 458 | "2": { 459 | "color": "", 460 | "label": "Quadrant 3" 461 | }, 462 | "3": { 463 | "color": "", 464 | "label": "Quadrant 4" 465 | } 466 | }, 467 | "quadrants_enabled": false, 468 | "series_labels": {}, 469 | "series_types": {}, 470 | "show_null_points": false, 471 | "show_value_labels": false, 472 | "show_view_names": true, 473 | "show_x_axis_label": true, 474 | "show_x_axis_ticks": true, 475 | "show_y_axis_labels": true, 476 | "show_y_axis_ticks": true, 477 | "size_by_field": "roi", 478 | "stacking": "normal", 479 | "swap_axes": true, 480 | "trellis": "", 481 | "type": "looker_scatter", 482 | "x_axis_gridlines": false, 483 | "x_axis_reversed": false, 484 | "x_axis_scale": "auto", 485 | "x_axis_zoom": true, 486 | "y_axes": [ 487 | { 488 | "label": "", 489 | "orientation": "bottom", 490 | "series": [ 491 | { 492 | "axisId": "Channel_0 - average_of_roi_first", 493 | "id": "Channel_0 - average_of_roi_first", 494 | "name": "Channel_0" 495 | }, 496 | { 497 | "axisId": "Channel_1 - average_of_roi_first", 498 | "id": "Channel_1 - average_of_roi_first", 499 | "name": "Channel_1" 500 | }, 501 | { 502 | "axisId": "Channel_2 - average_of_roi_first", 503 | "id": "Channel_2 - average_of_roi_first", 504 | "name": "Channel_2" 505 | }, 506 | { 507 | "axisId": "Channel_3 - average_of_roi_first", 508 | "id": "Channel_3 - average_of_roi_first", 509 | "name": "Channel_3" 510 | }, 511 | { 512 | "axisId": "Channel_4 - average_of_roi_first", 513 | "id": "Channel_4 - average_of_roi_first", 514 | "name": "Channel_4" 515 | } 516 | ], 517 | "showLabels": true, 518 | "showValues": true, 519 | "tickDensity": "custom", 520 | "tickDensityCustom": 100, 521 | "type": "linear", 522 | "unpinAxis": false 523 | } 524 | ], 525 | "y_axis_combined": true, 526 | "y_axis_gridlines": true, 527 | "y_axis_reversed": false, 528 | "y_axis_scale_mode": "linear", 529 | "y_axis_tick_density": "default", 530 | "y_axis_tick_density_custom": 5, 531 | "y_axis_zoom": true 532 | }} 533 | 534 | A single record visualization - 535 | {{ 536 | "defaults_version": 1, 537 | "show_view_names": false, 538 | "type": "looker_single_record" 539 | }} 540 | 541 | A single value visualization - 542 | {{ 543 | "comparison_reverse_colors": false, 544 | "comparison_type": "value", "conditional_formatting_include_nulls": false, "conditional_formatting_include_totals": false, 545 | "custom_color": "#1A73E8", 546 | "custom_color_enabled": true, 547 | "defaults_version": 1, 548 | "enable_conditional_formatting": false, 549 | "series_types": {}, 550 | "show_comparison": false, 551 | "show_comparison_label": true, 552 | "show_single_value_title": true, 553 | "single_value_title": "Total Clicks", 554 | "type": "single_value" 555 | }} 556 | 557 | A Pie chart - 558 | {{ 559 | "defaults_version": 1, 560 | "label_density": 25, 561 | "label_type": "labPer", 562 | "legend_position": "center", 563 | "limit_displayed_rows": false, 564 | "ordering": "none", 565 | "plot_size_by_field": false, 566 | "point_style": "none", 567 | "series_types": {}, 568 | "show_null_labels": false, 569 | "show_silhouette": false, 570 | "show_totals_labels": false, 571 | "show_value_labels": false, 572 | "show_view_names": false, 573 | "show_x_axis_label": true, 574 | "show_x_axis_ticks": true, 575 | "show_y_axis_labels": true, 576 | "show_y_axis_ticks": true, 577 | "stacking": "", 578 | "totals_color": "#808080", 579 | "trellis": "", 580 | "type": "looker_pie", 581 | "value_labels": "legend", 582 | "x_axis_gridlines": false, 583 | "x_axis_reversed": false, 584 | "x_axis_scale": "auto", 585 | "y_axis_combined": true, 586 | "y_axis_gridlines": true, 587 | "y_axis_reversed": false, 588 | "y_axis_scale_mode": "linear", 589 | "y_axis_tick_density": "default", 590 | "y_axis_tick_density_custom": 5 591 | }} 592 | 593 | The result is a JSON object with the id, slug, the url, and 594 | the long_url. 595 | 596 | get_looks: 597 | kind: looker-get-looks 598 | source: looker-source 599 | description: | 600 | get_looks Tool 601 | 602 | This tool is used to search for saved looks in a Looker instance. 603 | String search params use case-insensitive matching. String search 604 | params can contain % and '_' as SQL LIKE pattern match wildcard 605 | expressions. example="dan%" will match "danger" and "Danzig" but 606 | not "David" example="D_m%" will match "Damage" and "dump". 607 | 608 | Most search params can accept "IS NULL" and "NOT NULL" as special 609 | expressions to match or exclude (respectively) rows where the 610 | column is null. 611 | 612 | The limit and offset are used to paginate the results. 613 | 614 | The result of the get_looks tool is a list of json objects. 615 | 616 | run_look: 617 | kind: looker-run-look 618 | source: looker-source 619 | description: | 620 | run_look Tool 621 | 622 | This tool runs the query associated with a look and returns 623 | the data in a JSON structure. It accepts the look_id as the 624 | parameter. 625 | 626 | make_look: 627 | kind: looker-make-look 628 | source: looker-source 629 | description: | 630 | make_look Tool 631 | 632 | This tool creates a new look in Looker, using the query 633 | parameters and the vis_config specified. 634 | 635 | Most of the parameters are the same as the query_url 636 | tool. In addition, there is a title and a description 637 | that must be provided. 638 | 639 | The newly created look will be created in the user's 640 | personal folder in looker. The look name must be unique. 641 | 642 | The result is a json document with a link to the newly 643 | created look. 644 | 645 | get_dashboards: 646 | kind: looker-get-dashboards 647 | source: looker-source 648 | description: | 649 | get_dashboards Tool 650 | 651 | This tool is used to search for saved dashboards in a Looker instance. 652 | String search params use case-insensitive matching. String search 653 | params can contain % and '_' as SQL LIKE pattern match wildcard 654 | expressions. example="dan%" will match "danger" and "Danzig" but 655 | not "David" example="D_m%" will match "Damage" and "dump". 656 | Most search params can accept "IS NULL" and "NOT NULL" as special 657 | expressions to match or exclude (respectively) rows where the 658 | column is null. 659 | 660 | The limit and offset are used to paginate the results. 661 | 662 | The result of the get_dashboards tool is a list of json objects. 663 | 664 | make_dashboard: 665 | kind: looker-make-dashboard 666 | source: looker-source 667 | description: | 668 | make_dashboard Tool 669 | 670 | This tool creates a new dashboard in Looker. The dashboard is 671 | initially empty and the add_dashboard_element tool is used to 672 | add content to the dashboard. 673 | 674 | The newly created dashboard will be created in the user's 675 | personal folder in looker. The dashboard name must be unique. 676 | 677 | The result is a json document with a link to the newly 678 | created dashboard and the id of the dashboard. Use the id 679 | when calling add_dashboard_element. 680 | 681 | add_dashboard_element: 682 | kind: looker-add-dashboard-element 683 | source: looker-source 684 | description: | 685 | add_dashboard_element Tool 686 | 687 | This tool creates a new tile in a Looker dashboard using 688 | the query parameters and the vis_config specified. 689 | 690 | Most of the parameters are the same as the query_url 691 | tool. In addition, there is a title that may be provided. 692 | The dashboard_id must be specified. That is obtained 693 | from calling make_dashboard. 694 | 695 | This tool can be called many times for one dashboard_id 696 | and the resulting tiles will be added in order. 697 | 698 | health_pulse: 699 | kind: looker-health-pulse 700 | source: looker-source 701 | description: | 702 | health-pulse Tool 703 | 704 | This tool takes the pulse of a Looker instance by taking 705 | one of the following actions: 706 | 1. `check_db_connections`, 707 | 2. `check_dashboard_performance`, 708 | 3. `check_dashboard_errors`, 709 | 4. `check_explore_performance`, 710 | 5. `check_schedule_failures`, or 711 | 6. `check_legacy_features` 712 | 713 | health_analyze: 714 | kind: looker-health-analyze 715 | source: looker-source 716 | description: | 717 | health-analyze Tool 718 | 719 | This tool calculates the usage of projects, models and explores. 720 | 721 | It accepts 6 parameters: 722 | 1. `action`: can be "projects", "models", or "explores" 723 | 2. `project`: the project to analyze (optional) 724 | 3. `model`: the model to analyze (optional) 725 | 4. `explore`: the explore to analyze (optional) 726 | 5. `timeframe`: the lookback period in days, default is 90 727 | 6. `min_queries`: the minimum number of queries to consider a resource as active, default is 1 728 | 729 | health_vacuum: 730 | kind: looker-health-vacuum 731 | source: looker-source 732 | description: | 733 | health-vacuum Tool 734 | 735 | This tool suggests models or explores that can removed 736 | because they are unused. 737 | 738 | It accepts 6 parameters: 739 | 1. `action`: can be "models" or "explores" 740 | 2. `project`: the project to vacuum (optional) 741 | 3. `model`: the model to vacuum (optional) 742 | 4. `explore`: the explore to vacuum (optional) 743 | 5. `timeframe`: the lookback period in days, default is 90 744 | 6. `min_queries`: the minimum number of queries to consider a resource as active, default is 1 745 | 746 | The result is a list of objects that are candidates for deletion. 747 | 748 | toolsets: 749 | looker_tools: 750 | - get_models 751 | - get_explores 752 | - get_dimensions 753 | - get_measures 754 | - get_filters 755 | - get_parameters 756 | - query 757 | - query_sql 758 | - query_url 759 | - get_looks 760 | - run_look 761 | - make_look 762 | - get_dashboards 763 | - make_dashboard 764 | - add_dashboard_element 765 | - health_pulse 766 | - health_analyze 767 | - health_vacuum ```