codecov[bot]: # [Codecov](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=h1) Report
> Merging [#217](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=desc) into [master](https://codecov.io/gh/spectacles-ci/spectacles/commit/bee3ec4fa8841f2f731e5745c25df3902763108d&el=desc) will **increase** coverage by `0.04%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/spectacles-ci/spectacles/pull/217/graphs/tree.svg?width=650&height=150&src=pr&token=EWgmmQti5j)](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #217      +/-   ##
==========================================
+ Coverage   89.37%   89.41%   +0.04%     
==========================================
  Files          11       11              
  Lines        1016     1020       +4     
==========================================
+ Hits          908      912       +4     
  Misses        108      108              
```


| [Impacted Files](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [spectacles/cli.py](https://codecov.io/gh/spectacles-ci/spectacles/pull/217/diff?src=pr&el=tree#diff-c3BlY3RhY2xlcy9jbGkucHk=) | `78.70% <100.00%> (+0.56%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=footer). Last update [bee3ec4...bb99800](https://codecov.io/gh/spectacles-ci/spectacles/pull/217?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/spectacles/cli.py b/spectacles/cli.py
index d0a8a73..9eb3447 100644
--- a/spectacles/cli.py
+++ b/spectacles/cli.py
@@ -1,6 +1,7 @@
 from pathlib import Path
 import sys
 import yaml
+import json
 from yaml.parser import ParserError
 import argparse
 import logging
@@ -9,7 +10,11 @@ from typing import Callable, Iterable, List
 from spectacles import __version__
 from spectacles.runner import Runner
 from spectacles.client import LookerClient
-from spectacles.exceptions import SpectaclesException, GenericValidationError
+from spectacles.exceptions import (
+    LookerApiError,
+    SpectaclesException,
+    GenericValidationError,
+)
 from spectacles.logger import GLOBAL_LOGGER as logger, set_file_handler
 import spectacles.printer as printer
 import spectacles.tracking as tracking
@@ -145,6 +150,23 @@ def handle_exceptions(function: Callable) -> Callable:
             return function(*args, **kwargs)
         except GenericValidationError as error:
             sys.exit(error.exit_code)
+        except LookerApiError as error:
+            logger.error(
+                f"\n{error}\n\n"
+                + printer.dim(
+                    "Run in verbose mode (-v) or check your log file to see the full "
+                    "response from the Looker API. "
+                    "For support, please create an issue at "
+                    "https://github.com/spectacles-ci/spectacles/issues"
+                )
+                + "\n"
+            )
+            looker_api_response = json.dumps(error.looker_api_response, indent=2)
+            logger.debug(
+                f"Spectacles received a {error.status} response code from "
+                f"the Looker API with the following details: {looker_api_response}\n"
+            )
+            sys.exit(error.exit_code)
         except SpectaclesException as error:
             logger.error(
                 f"\n{error}\n\n"

Log response from Looker in handle_exceptions
We should catch and handle `LookerApiError` specifically [here](https://github.com/spectacles-ci/spectacles/blob/master/spectacles/cli.py#L148).

The exception object has a `response` attribute which is the raw response from Looker. We can log (at a debug level) the response which will help a lot with debugging (even though, sometimes the message from Looker is not useful).
diff --git a/tests/test_cli.py b/tests/test_cli.py
index f4e655a..106f16f 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,8 +1,14 @@
-from unittest.mock import patch
+from unittest.mock import patch, Mock
+import logging
 import pytest
+import requests
 from tests.constants import ENV_VARS
 from spectacles.cli import main, create_parser, handle_exceptions
-from spectacles.exceptions import SpectaclesException, GenericValidationError
+from spectacles.exceptions import (
+    LookerApiError,
+    SpectaclesException,
+    GenericValidationError,
+)
 
 
 @pytest.fixture
@@ -57,6 +63,37 @@ def test_handle_exceptions_unhandled_error(exception, exit_code):
     assert pytest_error.value.code == exit_code
 
 
+def test_handle_exceptions_looker_error_should_log_response_and_status(caplog):
+    caplog.set_level(logging.DEBUG)
+    response = Mock(spec=requests.Response)
+    response.request = Mock(spec=requests.PreparedRequest)
+    response.request.url = "https://api.looker.com"
+    response.request.method = "GET"
+    response.json.return_value = {
+        "message": "Not found",
+        "documentation_url": "http://docs.looker.com/",
+    }
+    status = 404
+
+    @handle_exceptions
+    def raise_exception():
+        raise LookerApiError(
+            name="exception-name",
+            title="An exception occurred.",
+            detail="Couldn't handle the truth. Please try again.",
+            status=status,
+            response=response,
+        )
+
+    with pytest.raises(SystemExit) as pytest_error:
+        raise_exception()
+    captured = "\n".join(record.msg for record in caplog.records)
+    assert str(status) in captured
+    assert '"message": "Not found"' in captured
+    assert '"documentation_url": "http://docs.looker.com/"' in captured
+    assert pytest_error.value.code == 101
+
+
 def test_parse_args_with_no_arguments_supplied(clean_env, capsys):
     parser = create_parser()
     with pytest.raises(SystemExit):

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest mypy flake8 black pytest-cov pytest-recording pytest-randomly vcrpy jsonschema
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
analytics-python==1.2.9
attrs==25.3.0
backoff==1.8.0
black==25.1.0
certifi==2025.1.31
chardet==3.0.4
click==8.1.8
colorama==0.4.1
coverage==7.8.0
exceptiongroup==1.2.2
flake8==7.2.0
idna==2.8
importlib_metadata==8.6.1
iniconfig==2.1.0
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
mccabe==0.7.0
multidict==6.2.0
mypy==1.15.0
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
propcache==0.3.1
pycodestyle==2.13.0
pyflakes==3.3.1
pytest==8.3.5
pytest-cov==6.0.0
pytest-randomly==3.16.0
pytest-recording==0.13.2
python-dateutil==2.9.0.post0
PyYAML==5.1.2
referencing==0.36.2
requests==2.22.0
rpds-py==0.24.0
six==1.17.0
-e git+https://github.com/spectacles-ci/spectacles.git@c623255c6d55867509174c4109c6f25b6f912989#egg=spectacles
tomli==2.2.1
typing_extensions==4.13.0
urllib3==1.25.11
vcrpy==7.0.0
wrapt==1.17.2
yarl==1.18.3
zipp==3.21.0

name: spectacles
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - analytics-python==1.2.9
      - attrs==25.3.0
      - backoff==1.8.0
      - black==25.1.0
      - certifi==2025.1.31
      - chardet==3.0.4
      - click==8.1.8
      - colorama==0.4.1
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - idna==2.8
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jsonschema==4.23.0
      - jsonschema-specifications==2024.10.1
      - mccabe==0.7.0
      - multidict==6.2.0
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - propcache==0.3.1
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-randomly==3.16.0
      - pytest-recording==0.13.2
      - python-dateutil==2.9.0.post0
      - pyyaml==5.1.2
      - referencing==0.36.2
      - requests==2.22.0
      - rpds-py==0.24.0
      - six==1.17.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - urllib3==1.25.11
      - vcrpy==7.0.0
      - wrapt==1.17.2
      - yarl==1.18.3
      - zipp==3.21.0
prefix: /opt/conda/envs/spectacles

tests/test_cli.py::test_handle_exceptions_looker_error_should_log_response_and_status
tests/test_cli.py::test_main_with_sql_validator tests/test_cli.py::test_bad_config_file_parameter tests/test_cli.py::test_parse_args_with_one_argument_supplied tests/test_cli.py::test_parse_args_with_only_cli tests/test_cli.py::test_handle_exceptions_unhandled_error[SpectaclesException-100] tests/test_cli.py::test_parse_args_with_mutually_exclusive_args_remote_reset tests/test_cli.py::test_parse_args_with_incomplete_config_file tests/test_cli.py::test_parse_args_with_incomplete_env_vars tests/test_cli.py::test_handle_exceptions_unhandled_error[GenericValidationError-102] tests/test_cli.py::test_parse_args_with_only_config_file tests/test_cli.py::test_arg_precedence tests/test_cli.py::test_handle_exceptions_unhandled_error[ValueError-1] tests/test_cli.py::test_main_with_assert_validator tests/test_cli.py::test_main_with_connect tests/test_cli.py::test_parse_args_with_no_arguments_supplied tests/test_cli.py::test_parse_args_with_mutually_exclusive_args_commit_ref tests/test_cli.py::test_config_override_argparse_default tests/test_cli.py::test_env_var_override_argparse_default tests/test_cli.py::test_main_with_do_not_track tests/test_cli.py::test_parse_args_with_only_env_vars tests/test_cli.py::test_parse_remote_reset_with_assert tests/test_cli.py::test_help
spectacles/cli.py
spectacles/cli.py