codecov[bot]: # [Codecov](https://codecov.io/gh/spectacles-ci/spectacles/pull/192?src=pr&el=h1) Report
> Merging [#192](https://codecov.io/gh/spectacles-ci/spectacles/pull/192?src=pr&el=desc) into [feature/vcr](https://codecov.io/gh/spectacles-ci/spectacles/commit/9d7d3481e04b1bb22bdd16c7b42ce63da6255584&el=desc) will **increase** coverage by `0.84%`.
> The diff coverage is `78.78%`.

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

```diff
@@               Coverage Diff               @@
##           feature/vcr     #192      +/-   ##
===============================================
+ Coverage        81.06%   81.91%   +0.84%     
===============================================
  Files               10       10              
  Lines              882      879       -3     
===============================================
+ Hits               715      720       +5     
+ Misses             167      159       -8     
```


| [Impacted Files](https://codecov.io/gh/spectacles-ci/spectacles/pull/192?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [spectacles/runner.py](https://codecov.io/gh/spectacles-ci/spectacles/pull/192/diff?src=pr&el=tree#diff-c3BlY3RhY2xlcy9ydW5uZXIucHk=) | `38.77% <0.00%> (-4.41%)` | :arrow_down: |
| [spectacles/client.py](https://codecov.io/gh/spectacles-ci/spectacles/pull/192/diff?src=pr&el=tree#diff-c3BlY3RhY2xlcy9jbGllbnQucHk=) | `95.90% <100.00%> (+7.07%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/spectacles-ci/spectacles/pull/192?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/192?src=pr&el=footer). Last update [9d7d348...d252ec9](https://codecov.io/gh/spectacles-ci/spectacles/pull/192?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/spectacles/client.py b/spectacles/client.py
index 5157acd..7d83d8b 100644
--- a/spectacles/client.py
+++ b/spectacles/client.py
@@ -119,98 +119,84 @@ class LookerClient:
 
         return response.json()["looker_release_version"]
 
-    def update_session(
-        self, project: str, branch: str, remote_reset: bool = False
-    ) -> None:
-        """Switches to a development mode session and checks out the desired branch.
+    def update_workspace(self, project: str, workspace: str) -> None:
+        """Updates the session workspace.
 
         Args:
             project: Name of the Looker project to use.
-            branch: Name of the Git branch to check out.
+            workspace: The workspace to switch to, either 'production' or 'dev'
+        """
+        logger.debug("Updating session to use production workspace")
+        url = utils.compose_url(self.api_url, path=["session"])
+        body = {"workspace_id": workspace}
+        response = self.session.patch(url=url, json=body, timeout=TIMEOUT_SEC)
+        try:
+            response.raise_for_status()
+        except requests.exceptions.HTTPError:
+            raise LookerApiError(
+                name="unable-to-update-workspace",
+                title="Couldn't update the session's workspace.",
+                status=response.status_code,
+                detail=(
+                    f"Unable to update workspace to '{workspace}'. "
+                    "If you have any unsaved work on the branch "
+                    "checked out by the user whose API credentials "
+                    "Spectacles is using, please save it and try again."
+                ),
+                response=response,
+            )
+
+    def checkout_branch(self, project: str, branch: str) -> None:
+        """Checks out a new git branch. Only works in dev workspace.
 
+        Args:
+            project: Name of the Looker project to use.
+            branch: Name of the Git branch to check out.
         """
-        if branch == "master":
-            logger.debug("Updating session to use production workspace")
-            url = utils.compose_url(self.api_url, path=["session"])
-            body = {"workspace_id": "production"}
-            response = self.session.patch(url=url, json=body, timeout=TIMEOUT_SEC)
-            try:
-                response.raise_for_status()
-            except requests.exceptions.HTTPError:
-                raise LookerApiError(
-                    name="unable-to-set-prod",
-                    title="Couldn't update session to production mode.",
-                    status=response.status_code,
-                    detail=(
-                        "If you have any unsaved work on the branch "
-                        "checked out by the user whose API credentials "
-                        "Spectacles is using, please save it and try again."
-                    ),
-                    response=response,
-                )
+        logger.debug(f"Setting Git branch to {branch}")
+        url = utils.compose_url(self.api_url, path=["projects", project, "git_branch"])
+        body = {"name": branch}
+        response = self.session.put(url=url, json=body, timeout=TIMEOUT_SEC)
+        try:
+            response.raise_for_status()
+        except requests.exceptions.HTTPError:
+            raise LookerApiError(
+                name="unable-to-checkout-branch",
+                title="Couldn't checkout Git branch.",
+                status=response.status_code,
+                detail=(
+                    f"Unable to checkout Git branch '{branch}'. "
+                    "If you have uncommitted changes on the current branch, "
+                    "please commit or revert them, then try again."
+                ),
+                response=response,
+            )
 
-        else:
-            logger.debug("Updating session to use development workspace")
-            url = utils.compose_url(self.api_url, path=["session"])
-            body = {"workspace_id": "dev"}
-            response = self.session.patch(url=url, json=body, timeout=TIMEOUT_SEC)
-            try:
-                response.raise_for_status()
-            except requests.exceptions.HTTPError:
-                raise LookerApiError(
-                    name="unable-to-set-dev",
-                    title="Couldn't update session to development mode.",
-                    status=response.status_code,
-                    detail=(
-                        "If you have any unsaved work on the branch "
-                        "checked out by the user whose API credentials "
-                        "Spectacles is using, please save it and try again."
-                    ),
-                    response=response,
-                )
-
-            logger.debug(f"Setting Git branch to {branch}")
-            url = utils.compose_url(
-                self.api_url, path=["projects", project, "git_branch"]
+    def reset_to_remote(self, project: str) -> None:
+        """Reset a project development branch to the revision of the project that is on the remote.
+
+        Args:
+            project: Name of the Looker project to use.
+
+        """
+        logger.debug(f"Resetting branch to remote.")
+        url = utils.compose_url(
+            self.api_url, path=["projects", project, "reset_to_remote"]
+        )
+        response = self.session.post(url=url, timeout=TIMEOUT_SEC)
+        try:
+            response.raise_for_status()
+        except requests.exceptions.HTTPError:
+            raise LookerApiError(
+                name="unable-to-reset-remote",
+                title="Couldn't checkout Git branch.",
+                status=response.status_code,
+                detail=(
+                    f"Unable to reset local Git branch"
+                    "to match remote. Please try again."
+                ),
+                response=response,
             )
-            body = {"name": branch}
-            response = self.session.put(url=url, json=body, timeout=TIMEOUT_SEC)
-            try:
-                response.raise_for_status()
-            except requests.exceptions.HTTPError:
-                raise LookerApiError(
-                    name="unable-to-checkout-branch",
-                    title="Couldn't checkout Git branch.",
-                    status=response.status_code,
-                    detail=(
-                        f"Unable to checkout Git branch '{branch}'. "
-                        "If you have uncommitted changes on the current branch, "
-                        "please commit or revert them, then try again."
-                    ),
-                    response=response,
-                )
-
-            if remote_reset:
-                logger.debug(f"Resetting branch {branch} to remote.")
-                url = utils.compose_url(
-                    self.api_url, path=["projects", project, "reset_to_remote"]
-                )
-                response = self.session.post(url=url, timeout=TIMEOUT_SEC)
-                try:
-                    response.raise_for_status()
-                except requests.exceptions.HTTPError:
-                    raise LookerApiError(
-                        name="unable-to-reset-remote",
-                        title="Couldn't checkout Git branch.",
-                        status=response.status_code,
-                        detail=(
-                            f"Unable to reset local Git branch '{branch}' "
-                            "to match remote. Please try again."
-                        ),
-                        response=response,
-                    )
-
-            logger.info(f"Checked out branch {branch}")
 
     def get_manifest(self, project: str) -> JsonDict:
         """Gets all the dependent LookML projects defined in the manifest file.
diff --git a/spectacles/runner.py b/spectacles/runner.py
index 7afe397..45690c1 100644
--- a/spectacles/runner.py
+++ b/spectacles/runner.py
@@ -25,7 +25,7 @@ def manage_dependent_branches(fn: Callable) -> Callable:
             response = fn(self, *args, **kwargs)
 
             for project in local_dependencies:
-                self.client.update_session(project["name"], project["active_branch"])
+                self.client.checkout_branch(project["name"], project["active_branch"])
                 self.client.delete_branch(project["name"], project["temp_branch"])
 
         else:
@@ -70,7 +70,13 @@ class Runner:
         self.client = LookerClient(
             base_url, client_id, client_secret, port, api_version
         )
-        self.client.update_session(project, branch, remote_reset)
+        if branch == "master":
+            self.client.update_workspace(project, "production")
+        else:
+            self.client.update_workspace(project, "dev")
+            self.client.checkout_branch(project, branch)
+            if remote_reset:
+                self.client.reset_to_remote(project)
 
     @manage_dependent_branches
     @log_duration

Change client methods to have one to one relationship with endpoints
Methods like `update_session` currently do multiple things. We should split these out and move the logic on running them together to the runner.
diff --git a/tests/cassettes/init_runner.yaml b/tests/cassettes/init_runner.yaml
new file mode 100644
index 0000000..b9790a3
--- /dev/null
+++ b/tests/cassettes/init_runner.yaml
@@ -0,0 +1,470 @@
+interactions:
+- request:
+    body: ''
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '69'
+      Content-Type:
+      - application/x-www-form-urlencoded
+      User-Agent:
+      - python-requests/2.22.0
+    method: POST
+    uri: https://spectacles.looker.com:19999/api/3.1/login
+  response:
+    body:
+      string: '{"access_token":"BrBWFhsfXkJbtbZJyqsvGP54nYSJtX5byJ4n4Ngz","token_type":"Bearer","expires_in":3600}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:16:32 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://spectacles.looker.com:19999/api/3.1/versions
+  response:
+    body:
+      string: '{"looker_release_version":"7.6.17","current_version":{"version":"3.1","full_version":"3.1.0","status":"current","swagger_url":"https://spectacles.looker.com:19999/api/3.1/swagger.json"},"supported_versions":[{"version":"2.99","full_version":"2.99.0","status":"internal_test","swagger_url":"https://spectacles.looker.com:19999/api/2.99/swagger.json"},{"version":"3.0","full_version":"3.0.0","status":"legacy","swagger_url":"https://spectacles.looker.com:19999/api/3.0/swagger.json"},{"version":"3.1","full_version":"3.1.0","status":"current","swagger_url":"https://spectacles.looker.com:19999/api/3.1/swagger.json"},{"version":"4.0","full_version":"4.0.7.6","status":"experimental","swagger_url":"https://spectacles.looker.com:19999/api/4.0/swagger.json"}]}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '756'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:16:32 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"workspace_id": "dev"}'
+    headers:
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+    method: PATCH
+    uri: https://spectacles.looker.com:19999/api/3.1/session
+  response:
+    body:
+      string: '{"workspace_id":"dev","sudo_user_id":null,"can":{"view":true,"update":true}}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '76'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:16:32 GMT
+      Server:
+      - nginx
+      Set-Cookie:
+      - looker.browser=1155; expires=Tue, 02 May 2023 21:16:32 -0000; HttpOnly
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"name": "pytest"}'
+    headers:
+      Content-Length:
+      - '18'
+      Content-Type:
+      - application/json
+      Cookie:
+      - looker.browser=1155
+    method: PUT
+    uri: https://spectacles.looker.com:19999/api/3.1/projects/eye_exam/git_branch
+  response:
+    body:
+      string: '{"name":"pytest","remote":"origin","remote_name":"pytest","error":null,"message":null,"owner_name":null,"readonly":false,"personal":false,"is_local":true,"is_remote":true,"is_production":false,"ahead_count":0,"behind_count":0,"commit_at":1588340597,"ref":"176721285e47e0adac1b3ff55a26ca0346d5bfc7","remote_ref":"176721285e47e0adac1b3ff55a26ca0346d5bfc7","can":{}}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '363'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:16:33 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: ''
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '69'
+      Content-Type:
+      - application/x-www-form-urlencoded
+      User-Agent:
+      - python-requests/2.22.0
+    method: POST
+    uri: https://spectacles.looker.com:19999/api/3.1/login
+  response:
+    body:
+      string: '{"access_token":"TXYG5Hq8nYJBmkTYpB6drddypCT8mPk8t8XW2dwm","token_type":"Bearer","expires_in":3600}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:23:21 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://spectacles.looker.com:19999/api/3.1/versions
+  response:
+    body:
+      string: '{"looker_release_version":"7.6.17","current_version":{"version":"3.1","full_version":"3.1.0","status":"current","swagger_url":"https://spectacles.looker.com:19999/api/3.1/swagger.json"},"supported_versions":[{"version":"2.99","full_version":"2.99.0","status":"internal_test","swagger_url":"https://spectacles.looker.com:19999/api/2.99/swagger.json"},{"version":"3.0","full_version":"3.0.0","status":"legacy","swagger_url":"https://spectacles.looker.com:19999/api/3.0/swagger.json"},{"version":"3.1","full_version":"3.1.0","status":"current","swagger_url":"https://spectacles.looker.com:19999/api/3.1/swagger.json"},{"version":"4.0","full_version":"4.0.7.6","status":"experimental","swagger_url":"https://spectacles.looker.com:19999/api/4.0/swagger.json"}]}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '756'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:23:21 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"workspace_id": "dev"}'
+    headers:
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+    method: PATCH
+    uri: https://spectacles.looker.com:19999/api/3.1/session
+  response:
+    body:
+      string: '{"workspace_id":"dev","sudo_user_id":null,"can":{"view":true,"update":true}}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '76'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:23:22 GMT
+      Server:
+      - nginx
+      Set-Cookie:
+      - looker.browser=1159; expires=Tue, 02 May 2023 21:23:22 -0000; HttpOnly
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"name": "pytest"}'
+    headers:
+      Content-Length:
+      - '18'
+      Content-Type:
+      - application/json
+      Cookie:
+      - looker.browser=1159
+    method: PUT
+    uri: https://spectacles.looker.com:19999/api/3.1/projects/eye_exam/git_branch
+  response:
+    body:
+      string: '{"name":"pytest","remote":"origin","remote_name":"pytest","error":null,"message":null,"owner_name":null,"readonly":false,"personal":false,"is_local":true,"is_remote":true,"is_production":false,"ahead_count":0,"behind_count":0,"commit_at":1588340597,"ref":"176721285e47e0adac1b3ff55a26ca0346d5bfc7","remote_ref":"176721285e47e0adac1b3ff55a26ca0346d5bfc7","can":{}}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '363'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:23:22 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Cookie:
+      - looker.browser=1159
+    method: GET
+    uri: https://spectacles.looker.com:19999/api/3.1/projects/eye_exam/manifest
+  response:
+    body:
+      string: '{"message":"An error has occurred.","documentation_url":"http://docs.looker.com/"}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '82'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:23:22 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 500
+      message: Internal Server Error
+- request:
+    body: ''
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '69'
+      Content-Type:
+      - application/x-www-form-urlencoded
+      User-Agent:
+      - python-requests/2.22.0
+    method: POST
+    uri: https://spectacles.looker.com:19999/api/3.1/login
+  response:
+    body:
+      string: '{"access_token":"Q6C846vwrJT7mbRhMfZzg3sppzvmhvVKkXjbS4ZS","token_type":"Bearer","expires_in":3600}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:24:06 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://spectacles.looker.com:19999/api/3.1/versions
+  response:
+    body:
+      string: '{"looker_release_version":"7.6.17","current_version":{"version":"3.1","full_version":"3.1.0","status":"current","swagger_url":"https://spectacles.looker.com:19999/api/3.1/swagger.json"},"supported_versions":[{"version":"2.99","full_version":"2.99.0","status":"internal_test","swagger_url":"https://spectacles.looker.com:19999/api/2.99/swagger.json"},{"version":"3.0","full_version":"3.0.0","status":"legacy","swagger_url":"https://spectacles.looker.com:19999/api/3.0/swagger.json"},{"version":"3.1","full_version":"3.1.0","status":"current","swagger_url":"https://spectacles.looker.com:19999/api/3.1/swagger.json"},{"version":"4.0","full_version":"4.0.7.6","status":"experimental","swagger_url":"https://spectacles.looker.com:19999/api/4.0/swagger.json"}]}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '756'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:24:06 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"workspace_id": "dev"}'
+    headers:
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+    method: PATCH
+    uri: https://spectacles.looker.com:19999/api/3.1/session
+  response:
+    body:
+      string: '{"workspace_id":"dev","sudo_user_id":null,"can":{"view":true,"update":true}}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '76'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:24:06 GMT
+      Server:
+      - nginx
+      Set-Cookie:
+      - looker.browser=1160; expires=Tue, 02 May 2023 21:24:06 -0000; HttpOnly
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"name": "pytest"}'
+    headers:
+      Content-Length:
+      - '18'
+      Content-Type:
+      - application/json
+      Cookie:
+      - looker.browser=1160
+    method: PUT
+    uri: https://spectacles.looker.com:19999/api/3.1/projects/eye_exam/git_branch
+  response:
+    body:
+      string: '{"name":"pytest","remote":"origin","remote_name":"pytest","error":null,"message":null,"owner_name":null,"readonly":false,"personal":false,"is_local":true,"is_remote":true,"is_production":false,"ahead_count":0,"behind_count":0,"commit_at":1588340597,"ref":"176721285e47e0adac1b3ff55a26ca0346d5bfc7","remote_ref":"176721285e47e0adac1b3ff55a26ca0346d5bfc7","can":{}}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '363'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:24:07 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Cookie:
+      - looker.browser=1160
+    method: GET
+    uri: https://spectacles.looker.com:19999/api/3.1/projects/eye_exam/manifest
+  response:
+    body:
+      string: '{"message":"An error has occurred.","documentation_url":"http://docs.looker.com/"}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '82'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:24:07 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 500
+      message: Internal Server Error
+- request:
+    body: null
+    headers:
+      Cookie:
+      - looker.browser=1160
+    method: GET
+    uri: https://spectacles.looker.com:19999/api/3.1/projects/eye_exam/manifest
+  response:
+    body:
+      string: '{"message":"An error has occurred.","documentation_url":"http://docs.looker.com/"}'
+    headers:
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '82'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 02 May 2020 21:24:07 GMT
+      Server:
+      - nginx
+      Vary:
+      - Accept-Encoding
+      X-Content-Type-Options:
+      - nosniff
+    status:
+      code: 500
+      message: Internal Server Error
+version: 1
diff --git a/tests/conftest.py b/tests/conftest.py
index 1faac56..be1759d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,6 +6,7 @@ from spectacles.client import LookerClient
 from spectacles.exceptions import SqlError
 from spectacles.lookml import Project, Model, Explore, Dimension
 from tests.utils import load_resource
+from spectacles.runner import Runner
 
 
 @pytest.fixture(scope="session")
@@ -26,10 +27,28 @@ def looker_client(record_mode) -> Iterable[LookerClient]:
             client_id=os.environ.get("LOOKER_CLIENT_ID", ""),
             client_secret=os.environ.get("LOOKER_CLIENT_SECRET", ""),
         )
-        client.update_session(project="eye_exam", branch="master", remote_reset=False)
+        client.update_workspace(project="eye_exam", workspace="production")
         yield client
 
 
+@pytest.fixture(scope="session")
+def runner(record_mode) -> Iterable[Runner]:
+    with vcr.use_cassette(
+        "tests/cassettes/init_runner.yaml",
+        filter_post_data_parameters=["client_id", "client_secret"],
+        filter_headers=["Authorization"],
+        record_mode=record_mode,
+    ):
+        runner = Runner(
+            project="eye_exam",
+            branch="pytest",
+            base_url="https://spectacles.looker.com",
+            client_id=os.environ.get("LOOKER_CLIENT_ID", ""),
+            client_secret=os.environ.get("LOOKER_CLIENT_SECRET", ""),
+        )
+        yield runner
+
+
 @pytest.fixture
 def dimension():
     return Dimension(
diff --git a/tests/test_client.py b/tests/test_client.py
index 6f371bd..4fb6740 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -40,7 +40,9 @@ def client_kwargs():
     return dict(
         authenticate={"client_id": "", "client_secret": "", "api_version": 3.1},
         get_looker_release_version={},
-        update_session={"project": "project_name", "branch": "branch_name"},
+        update_workspace={"project": "project_name", "workspace": "dev"},
+        checkout_branch={"project": "project_name", "branch": "branch_name"},
+        reset_to_remote={"project": "project_name"},
         all_lookml_tests={"project": "project_name"},
         run_lookml_test={"project": "project_name"},
         get_lookml_models={},
@@ -63,10 +65,9 @@ def client_kwargs():
 @pytest.mark.vcr(match_on=["uri", "method", "raw_body"])
 def test_branch_management_should_work(looker_client):
     project = "eye_exam"
-    tmp_branch = "tmp-pytest"
-    looker_client.update_session(
-        project="eye_exam", branch="pytest", remote_reset=False
-    )
+    tmp_branch = f"tmp-pytest"
+    looker_client.update_workspace(project=project, workspace="dev")
+    looker_client.checkout_branch(project=project, branch="pytest")
     looker_client.create_branch("eye_exam", tmp_branch)
     try:
         looker_client.update_branch(project, tmp_branch, "master")
@@ -76,7 +77,7 @@ def test_branch_management_should_work(looker_client):
         # Return to the master branch and delete the temp branch
         looker_client.update_branch(project, "master")
         looker_client.delete_branch(project, tmp_branch)
-    looker_client.update_session(project=project, branch="master", remote_reset=False)
+    looker_client.update_workspace(project=project, workspace="production")
 
 
 @pytest.mark.vcr
diff --git a/tests/test_runner.py b/tests/test_runner.py
index e69de29..f1b3b6e 100644
--- a/tests/test_runner.py
+++ b/tests/test_runner.py
@@ -0,0 +1,14 @@
+import pytest
+from spectacles.exceptions import LookerApiError
+
+
+def test_validate_sql_with_import_projects_error(runner):
+    runner.import_projects = True
+    with pytest.raises(LookerApiError):
+        runner.validate_sql(["*/*"], [])
+
+
+def test_validate_assert_with_import_projects_error(runner):
+    runner.import_projects = True
+    with pytest.raises(LookerApiError):
+        runner.validate_sql()
diff --git a/tests/test_sql_validator.py b/tests/test_sql_validator.py
index fb67e2c..f08f1ba 100644
--- a/tests/test_sql_validator.py
+++ b/tests/test_sql_validator.py
@@ -56,8 +56,8 @@ class TestBuildUnconfiguredProject:
 
     def test_project_with_no_configured_models_should_raise_error(self, validator):
         validator.project.name = "eye_exam_unconfigured"
-        validator.client.update_session(
-            project="eye_exam_unconfigured", branch="master", remote_reset=False
+        validator.client.update_workspace(
+            project="eye_exam_unconfigured", workspace="production"
         )
         with pytest.raises(SpectaclesException):
             validator.build_project()

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
aiohappyeyeballs==2.6.1
aiohttp==3.11.14
aiosignal==1.3.2
analytics-python==1.2.9
async-timeout==5.0.1
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
frozenlist==1.5.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@12424471a27622f7be5113975a78004b8d241d27#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:
      - aiohappyeyeballs==2.6.1
      - aiohttp==3.11.14
      - aiosignal==1.3.2
      - analytics-python==1.2.9
      - async-timeout==5.0.1
      - 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
      - frozenlist==1.5.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_client.py::test_bad_requests_should_raise_looker_api_errors[get_active_branch] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[get_manifest] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[get_query_task_multi_results] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[update_workspace] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[run_lookml_test] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[create_branch] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[reset_to_remote] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[create_query_task] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[delete_branch] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[create_query] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[get_lookml_dimensions] tests/test_client.py::test_create_query_without_dimensions_should_return_certain_fields tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[get_looker_release_version] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[all_lookml_tests] tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[checkout_branch] tests/test_client.py::test_create_query_with_dimensions_should_return_certain_fields tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[update_branch] tests/test_client.py::test_get_looker_release_version_should_return_correct_version tests/test_client.py::test_branch_management_should_work tests/test_client.py::test_bad_requests_should_raise_looker_api_errors[get_lookml_models] tests/test_sql_validator.py::TestValidateFail::test_results_should_conform_to_schema tests/test_sql_validator.py::TestValidateFail::test_in_hybrid_mode_should_run_n_queries tests/test_sql_validator.py::TestValidateFail::test_should_set_errored_and_queried tests/test_sql_validator.py::TestValidateFail::test_running_queries_should_be_empty tests/test_sql_validator.py::test_extract_error_details_error_loc_wo_line tests/test_sql_validator.py::test_extract_error_details_error_list tests/test_sql_validator.py::test_cancel_queries tests/test_sql_validator.py::test_extract_error_details_error_dict tests/test_sql_validator.py::test_extract_error_details_no_message_details tests/test_sql_validator.py::test_get_running_query_tasks tests/test_sql_validator.py::test_extract_error_details_error_other tests/test_sql_validator.py::test_handle_running_query tests/test_sql_validator.py::test_extract_error_details_error_non_str_message_details tests/test_sql_validator.py::test_create_and_run_keyboard_interrupt_cancels_queries tests/test_sql_validator.py::TestBuildUnconfiguredProject::test_project_with_no_configured_models_should_raise_error tests/test_sql_validator.py::TestValidatePass::test_count_explores[batch] tests/test_sql_validator.py::TestValidatePass::test_in_single_mode_should_run_n_queries[single] tests/test_sql_validator.py::TestValidatePass::test_should_set_errored_and_queried[batch] tests/test_sql_validator.py::TestValidatePass::test_ignored_dimensions_are_not_queried[batch] tests/test_sql_validator.py::TestValidatePass::test_running_queries_should_be_empty[batch] tests/test_sql_validator.py::TestValidatePass::test_in_batch_mode_should_run_one_query[batch] tests/test_sql_validator.py::TestValidatePass::test_in_hybrid_or_single_mode_dimensions_should_be_queried[hybrid] tests/test_sql_validator.py::TestValidatePass::test_results_should_conform_to_schema[batch] tests/test_sql_validator.py::TestValidatePass::test_in_hybrid_mode_should_run_one_query[hybrid] tests/test_sql_validator.py::TestValidatePass::test_in_hybrid_or_single_mode_dimensions_should_be_queried[single] tests/test_sql_validator.py::TestValidatePass::test_running_queries_should_be_empty[single] tests/test_sql_validator.py::TestValidatePass::test_ignored_dimensions_are_not_queried[single] tests/test_sql_validator.py::TestValidatePass::test_should_set_errored_and_queried[single] tests/test_sql_validator.py::TestValidatePass::test_results_should_conform_to_schema[single] tests/test_sql_validator.py::TestValidatePass::test_running_queries_should_be_empty[hybrid] tests/test_sql_validator.py::TestValidatePass::test_should_set_errored_and_queried[hybrid] tests/test_sql_validator.py::TestBuildProject::test_duplicate_selectors_should_be_deduplicated tests/test_sql_validator.py::TestBuildProject::test_model_explore_dimension_counts_should_match tests/test_sql_validator.py::TestBuildProject::test_project_with_everything_excluded_should_not_have_models tests/test_sql_validator.py::TestBuildProject::test_invalid_model_selector_should_raise_error
tests/test_client.py::test_bad_authentication_request_should_raise_looker_api_error tests/test_client.py::test_authenticate_should_set_session_headers tests/test_client.py::test_unsupported_api_version_should_raise_error tests/test_sql_validator.py::test_parse_selectors_with_invalid_format_should_raise_error tests/test_sql_validator.py::test_parse_selectors_should_handle_duplicates tests/test_sql_validator.py::test_parse_selectors_should_handle_same_explore_in_different_model tests/test_runner.py::test_validate_assert_with_import_projects_error tests/test_runner.py::test_validate_sql_with_import_projects_error
spectacles/runner.py spectacles/client.py
spectacles/runner.py spectacles/client.py