Repository: tableau/server-client-python

Issue:
Unauthorized Access error when signing out (either explicitly or at the end of a `with` block)
Starting with v0.29 I notice many of my test scripts failing with an exception. It seems like a general problem rather than specific to one endpoint. Here's an example:

```py
import tableauserverclient as TSC

tableau_auth = TSC.PersonalAccessTokenAuth(
    "xxxxx",
    "xxxxxxxxxx",
    "",
)
server = TSC.Server("https://devplat.tableautest.com", use_server_version=True)

with server.auth.sign_in(tableau_auth):
    all_wb, pagination_item = server.workbooks.get()
    print("\nThere are {} workbooks: ".format(pagination_item.total_available))
    for wb in all_wb:
        print(wb.id, wb.name, wb.tags)
```

The script succeeds (printing workbooks), but ends with an exception like this:
```
Traceback (most recent call last):
  File "/Users/bcantoni/github/server-client-python/getdatasources.py", line 14, in <module>
    with server.auth.sign_in(tableau_auth):
  File "/Users/bcantoni/github/server-client-python/tableauserverclient/server/endpoint/auth_endpoint.py", line 27, in __exit__
    self._callback()
  File "/Users/bcantoni/github/server-client-python/tableauserverclient/server/endpoint/endpoint.py", line 291, in wrapper
    return func(self, *args, **kwargs)
  File "/Users/bcantoni/github/server-client-python/tableauserverclient/server/endpoint/auth_endpoint.py", line 85, in sign_out
    self.post_request(url, "")
  File "/Users/bcantoni/github/server-client-python/tableauserverclient/server/endpoint/endpoint.py", line 248, in post_request
    return self._make_request(
  File "/Users/bcantoni/github/server-client-python/tableauserverclient/server/endpoint/endpoint.py", line 165, in _make_request
    self._check_status(server_response, url)
  File "/Users/bcantoni/github/server-client-python/tableauserverclient/server/endpoint/endpoint.py", line 186, in _check_status
    raise NotSignedInError(server_response.content, url)
tableauserverclient.server.endpoint.exceptions.NotSignedInError: (b'<?xml version=\'1.0\' encoding=\'UTF-8\'?><tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_22.xsd"><error code="401002"><summary>Unauthorized Access</summary><detail>Invalid authentication credentials were provided.</detail></error></tsResponse>', 'https://10ax.online.tableau.com/api/3.22/auth/signout')
```

If I switch from using the `with` statement to the older style, it works fine:
```py
import tableauserverclient as TSC

tableau_auth = TSC.PersonalAccessTokenAuth(
    "xxxx",
    "xxxxxxx",
    "",
)
server = TSC.Server("https://devplat.tableautest.com", use_server_version=True)

server.auth.sign_in(tableau_auth)

all_wb, pagination_item = server.workbooks.get()
print("\nThere are {} workbooks: ".format(pagination_item.total_available))
for wb in all_wb:
    print(wb.id, wb.name, wb.tags)
```

Testing notes:

- I tested with same results on both Tableau Cloud and Server.
- This seems new to 0.29; switching back to 0.28 solves the issue.
- Git bisect points to https://github.com/tableau/server-client-python/pull/1300 as the point this was introduced
- In server/endpoint/endpoint.py, changing `seconds = 0.05` back to `seconds = 0` seems to fix it (but I'll admit I don't totally follow the changes in that PR)


Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index e9bd2b4..92778cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,3 +155,4 @@ $RECYCLE.BIN/
 docs/_site/
 docs/.jekyll-metadata
 docs/Gemfile.lock
+samples/credentials
diff --git a/contributing.md b/contributing.md
index 41c339c..6404611 100644
--- a/contributing.md
+++ b/contributing.md
@@ -10,8 +10,7 @@ Contribution can include, but are not limited to, any of the following:
 * Fix an Issue/Bug
 * Add/Fix documentation
 
-Contributions must follow the guidelines outlined on the [Tableau Organization](http://tableau.github.io/) page, though filing an issue or requesting
-a feature do not require the CLA.
+Contributions must follow the guidelines outlined on the [Tableau Organization](http://tableau.github.io/) page, though filing an issue or requesting a feature do not require the CLA.
 
 ## Issues and Feature Requests
 
diff --git a/samples/getting_started/3_hello_universe.py b/samples/getting_started/3_hello_universe.py
index 3ed39fd..21de978 100644
--- a/samples/getting_started/3_hello_universe.py
+++ b/samples/getting_started/3_hello_universe.py
@@ -62,11 +62,6 @@ def main():
             print("{} jobs".format(pagination.total_available))
             print(jobs[0])
 
-        metrics, pagination = server.metrics.get()
-        if metrics:
-            print("{} metrics".format(pagination.total_available))
-            print(metrics[0])
-
         schedules, pagination = server.schedules.get()
         if schedules:
             print("{} schedules".format(pagination.total_available))
@@ -82,7 +77,7 @@ def main():
             print("{} webhooks".format(pagination.total_available))
             print(webhooks[0])
 
-        users, pagination = server.metrics.get()
+        users, pagination = server.users.get()
         if users:
             print("{} users".format(pagination.total_available))
             print(users[0])
@@ -92,5 +87,6 @@ def main():
             print("{} groups".format(pagination.total_available))
             print(groups[0])
 
-    if __name__ == "__main__":
-        main()
+
+if __name__ == "__main__":
+    main()
diff --git a/tableauserverclient/models/interval_item.py b/tableauserverclient/models/interval_item.py
index f2f1596..537e6c1 100644
--- a/tableauserverclient/models/interval_item.py
+++ b/tableauserverclient/models/interval_item.py
@@ -69,7 +69,7 @@ class HourlyInterval(object):
 
     @interval.setter
     def interval(self, intervals):
-        VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12}
+        VALID_INTERVALS = {0.25, 0.5, 1, 2, 4, 6, 8, 12, 24}
         for interval in intervals:
             # if an hourly interval is a string, then it is a weekDay interval
             if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval):
diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py
index 77a7712..2b7f570 100644
--- a/tableauserverclient/server/endpoint/endpoint.py
+++ b/tableauserverclient/server/endpoint/endpoint.py
@@ -1,5 +1,3 @@
-from threading import Thread
-from time import sleep
 from tableauserverclient import datetime_helpers as datetime
 
 from packaging.version import Version
@@ -76,55 +74,20 @@ class Endpoint(object):
         return parameters
 
     def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]:
-        self.async_response = None
         response = None
         logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url))
         try:
             response = method(url, **parameters)
-            self.async_response = response
             logger.debug("[{}] Call finished".format(datetime.timestamp()))
         except Exception as e:
             logger.debug("Error making request to server: {}".format(e))
-            self.async_response = e
-        finally:
-            if response and not self.async_response:
-                logger.debug("Request response not saved")
-                return None
-        logger.debug("[{}] Request complete".format(datetime.timestamp()))
-        return self.async_response
+            raise e
+        return response
 
     def send_request_while_show_progress_threaded(
         self, method, url, parameters={}, request_timeout=None
     ) -> Optional[Union["Response", Exception]]:
-        try:
-            request_thread = Thread(target=self._blocking_request, args=(method, url, parameters))
-            request_thread.start()
-        except Exception as e:
-            logger.debug("Error starting server request on separate thread: {}".format(e))
-            return None
-        seconds = 0.05
-        minutes = 0
-        last_log_minute = 0
-        sleep(seconds)
-        if self.async_response is not None:
-            # a quick return for any immediate responses
-            return self.async_response
-        timed_out: bool = request_timeout is not None and seconds > request_timeout
-        while (self.async_response is None) and not timed_out:
-            sleep(DELAY_SLEEP_SECONDS)
-            seconds = seconds + DELAY_SLEEP_SECONDS
-            minutes = int(seconds / 60)
-            last_log_minute = self.log_wait_time(minutes, last_log_minute, url)
-        return self.async_response
-
-    def log_wait_time(self, minutes, last_log_minute, url) -> int:
-        logger.debug("{} Waiting....".format(datetime.timestamp()))
-        if minutes > last_log_minute:  # detailed log message ~every minute
-            logger.info("[{}] Waiting ({} minutes so far) for request to {}".format(datetime.timestamp(), minutes, url))
-            last_log_minute = minutes
-        else:
-            logger.debug("[{}] Waiting for request to {}".format(datetime.timestamp(), url))
-        return last_log_minute
+        return self._blocking_request(method, url, parameters)
 
     def _make_request(
         self,


Test patch:
diff --git a/test/test_endpoint.py b/test/test_endpoint.py
index 3d2d1c9..8635af9 100644
--- a/test/test_endpoint.py
+++ b/test/test_endpoint.py
@@ -1,4 +1,6 @@
 from pathlib import Path
+import pytest
+import requests
 import unittest
 
 import tableauserverclient as TSC
@@ -35,11 +37,12 @@ class TestEndpoint(unittest.TestCase):
             )
             self.assertIsNotNone(response)
 
-    def test_blocking_request_returns(self) -> None:
-        url = "http://test/"
-        endpoint = TSC.server.Endpoint(self.server)
-        response = endpoint._blocking_request(endpoint.parent_srv.session.get, url=url)
-        self.assertIsNotNone(response)
+    def test_blocking_request_raises_request_error(self) -> None:
+        with pytest.raises(requests.exceptions.ConnectionError):
+            url = "http://test/"
+            endpoint = TSC.server.Endpoint(self.server)
+            response = endpoint._blocking_request(endpoint.parent_srv.session.get, url=url)
+            self.assertIsNotNone(response)
 
     def test_get_request_stream(self) -> None:
         url = "http://test/"