Skip to content

Commit 659a634

Browse files
Updating Errors and Auth (#44)
1 parent 9d5c3e3 commit 659a634

20 files changed

+885
-82
lines changed

.circleci/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ jobs:
2121
steps:
2222
- checkout
2323
- run: pip install --user tox
24-
- run: tox -e py<< parameters.python_version >>-pydantic<< parameters.pydantic_version >>-requests<< parameters.requests_version >>
24+
- run: poetry --no-ansi install --no-root --sync
25+
- run: poetry --no-ansi run tox -v -e py<< parameters.python_version >>-pydantic<< parameters.pydantic_version >>-requests<< parameters.requests_version >> --recreate
2526

2627
pyright:
2728
docker:

changelog/@unreleased/pr-44.v2.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type: improvement
2+
improvement:
3+
description: Updating Errors and Auth
4+
links:
5+
- https://github.com/palantir/foundry-platform-python/pull/44

foundry/_core/confidential_client_auth.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
# limitations under the License.
1414

1515

16-
import asyncio
16+
import threading
17+
import time
1718
from typing import Callable
1819
from typing import List
1920
from typing import Optional
@@ -27,7 +28,6 @@
2728
from foundry._core.oauth_utils import ConfidentialClientOAuthFlowProvider
2829
from foundry._core.oauth_utils import OAuthToken
2930
from foundry._core.utils import remove_prefixes
30-
from foundry._errors.environment_not_configured import EnvironmentNotConfigured
3131
from foundry._errors.not_authenticated import NotAuthenticated
3232

3333
T = TypeVar("T")
@@ -56,7 +56,7 @@ def __init__(
5656
self._client_secret = client_secret
5757
self._token: Optional[OAuthToken] = None
5858
self._should_refresh = should_refresh
59-
self._refresh_task: Optional[asyncio.Task] = None
59+
self._stop_refresh_event = threading.Event()
6060
self._hostname = hostname
6161
self._server_oauth_flow_provider = ConfidentialClientOAuthFlowProvider(
6262
client_id, client_secret, self.url, scopes=scopes
@@ -70,15 +70,21 @@ def get_token(self) -> OAuthToken:
7070
def execute_with_token(self, func: Callable[[OAuthToken], T]) -> T:
7171
try:
7272
return self._run_with_attempted_refresh(func)
73+
except requests.HTTPError as http_e:
74+
if http_e.response.status_code == 401:
75+
self.sign_out()
76+
raise http_e
7377
except Exception as e:
74-
self.sign_out()
7578
raise e
7679

7780
def run_with_token(self, func: Callable[[OAuthToken], T]) -> None:
7881
try:
7982
self._run_with_attempted_refresh(func)
83+
except requests.HTTPError as http_e:
84+
if http_e.response.status_code == 401:
85+
self.sign_out()
86+
raise http_e
8087
except Exception as e:
81-
self.sign_out()
8288
raise e
8389

8490
def _run_with_attempted_refresh(self, func: Callable[[OAuthToken], T]) -> T:
@@ -89,45 +95,50 @@ def _run_with_attempted_refresh(self, func: Callable[[OAuthToken], T]) -> T:
8995
try:
9096
return func(self.get_token())
9197
except requests.HTTPError as e:
92-
if e.response is not None and e.response.status_code == 401:
98+
if e.response.status_code == 401:
9399
self._refresh_token()
94100
return func(self.get_token())
95101
else:
96102
raise e
97103

98104
@property
99-
def url(self):
105+
def url(self) -> str:
100106
return remove_prefixes(self._hostname, ["https://", "http://"])
101107

102-
def _refresh_token(self):
108+
def _refresh_token(self) -> None:
103109
self._token = self._server_oauth_flow_provider.get_token()
104110

111+
def _start_auto_refresh(self) -> None:
112+
def _auto_refresh_token() -> None:
113+
while not self._stop_refresh_event.is_set():
114+
if self._token:
115+
# Sleep for (expires_in - 60) seconds to refresh the token 1 minute before it expires
116+
time.sleep(self._token.expires_in - 60)
117+
self._refresh_token()
118+
else:
119+
# Wait 10 seconds and check again if the token is set
120+
time.sleep(10)
121+
122+
refresh_thread = threading.Thread(target=_auto_refresh_token, daemon=True)
123+
refresh_thread.start()
124+
105125
def sign_in_as_service_user(self) -> SignInResponse:
106126
token = self._server_oauth_flow_provider.get_token()
107127
self._token = token
108128

109-
async def refresh_token_task():
110-
while True:
111-
if self._token is None:
112-
raise RuntimeError("The token was None when trying to refresh.")
113-
114-
await asyncio.sleep(self._token.expires_in / 60 - 10)
115-
self._token = self._server_oauth_flow_provider.get_token()
116-
117129
if self._should_refresh:
118-
loop = asyncio.get_event_loop()
119-
self._refresh_task = loop.create_task(refresh_token_task())
130+
self._start_auto_refresh()
120131
return SignInResponse(
121132
session={"accessToken": token.access_token, "expiresIn": token.expires_in}
122133
)
123134

124135
def sign_out(self) -> SignOutResponse:
125-
if self._refresh_task:
126-
self._refresh_task.cancel()
127-
self._refresh_task = None
128-
129136
if self._token:
130137
self._server_oauth_flow_provider.revoke_token(self._token.access_token)
131138

132139
self._token = None
140+
141+
# Signal the auto-refresh thread to stop
142+
self._stop_refresh_event.set()
143+
133144
return SignOutResponse()

foundry/_core/oauth.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
# limitations under the License.
1414

1515

16+
from typing import Any
17+
from typing import Dict
18+
1619
from pydantic import BaseModel
1720

1821

1922
class SignInResponse(BaseModel):
20-
session: dict
23+
session: Dict[str, Any]
2124

2225

2326
class SignOutResponse(BaseModel):

foundry/_core/oauth_utils.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import secrets
1919
import string
2020
import time
21+
from typing import Any
22+
from typing import Dict
2123
from typing import List
2224
from typing import Optional
2325
from urllib.parse import urlencode
@@ -68,7 +70,7 @@ class OAuthTokenResponse(BaseModel):
6870
expires_in: int
6971
refresh_token: Optional[str] = None
7072

71-
def __init__(self, token_response: dict) -> None:
73+
def __init__(self, token_response: Dict[str, Any]) -> None:
7274
super().__init__(**token_response)
7375

7476

@@ -167,13 +169,13 @@ def get_scopes(self) -> List[str]:
167169
return scopes
168170

169171

170-
def generate_random_string(min_length=43, max_length=128):
172+
def generate_random_string(min_length: int = 43, max_length: int = 128) -> str:
171173
characters = string.ascii_letters + string.digits + "-._~"
172174
length = secrets.randbelow(max_length - min_length + 1) + min_length
173175
return "".join(secrets.choice(characters) for _ in range(length))
174176

175177

176-
def generate_code_challenge(input_string):
178+
def generate_code_challenge(input_string: str) -> str:
177179
# Calculate the SHA256 hash
178180
sha256_hash = hashlib.sha256(input_string.encode("utf-8")).digest()
179181

@@ -249,7 +251,7 @@ def get_token(self, code: str, code_verifier: str) -> OAuthToken:
249251
response.raise_for_status()
250252
return OAuthToken(token=OAuthTokenResponse(token_response=response.json()))
251253

252-
def refresh_token(self, refresh_token):
254+
def refresh_token(self, refresh_token: str) -> OAuthToken:
253255
headers = {"Content-Type": "application/x-www-form-urlencoded"}
254256
params = {
255257
"grant_type": "refresh_token",

foundry/_core/public_client_auth.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,21 @@
3636

3737

3838
class PublicClientAuth(Auth):
39-
scopes: List[str] = ["api:read-data", "api:write-data", "offline_access"]
40-
4139
"""
4240
Client for Public Client OAuth-authenticated Ontology applications.
4341
Runs a background thread to periodically refresh access token.
44-
4542
:param client_id: OAuth client id to be used by the application.
4643
:param client_secret: OAuth client secret to be used by the application.
4744
:param hostname: Hostname for authentication and ontology endpoints.
4845
"""
4946

5047
def __init__(
51-
self, client_id: str, redirect_url: str, hostname: str, should_refresh: bool = False
48+
self,
49+
client_id: str,
50+
redirect_url: str,
51+
hostname: str,
52+
scopes: Optional[List[str]] = None,
53+
should_refresh: bool = False,
5254
) -> None:
5355
self._client_id = client_id
5456
self._redirect_url = redirect_url
@@ -58,7 +60,7 @@ def __init__(
5860
self._stop_refresh_event = threading.Event()
5961
self._hostname = hostname
6062
self._server_oauth_flow_provider = PublicClientOAuthFlowProvider(
61-
client_id=client_id, redirect_url=redirect_url, url=self.url, scopes=self.scopes
63+
client_id=client_id, redirect_url=redirect_url, url=self.url, scopes=scopes
6264
)
6365
self._auth_request: Optional[AuthorizeRequest] = None
6466

@@ -81,9 +83,11 @@ def run_with_token(self, func: Callable[[OAuthToken], T]) -> None:
8183
self.sign_out()
8284
raise e
8385

84-
def _refresh_token(self):
85-
if self._token is None:
86-
raise Exception("")
86+
def _refresh_token(self) -> None:
87+
if not self._token:
88+
raise RuntimeError("must have token to refresh")
89+
if not self._token.refresh_token:
90+
raise RuntimeError("no refresh token provided")
8791

8892
self._token = self._server_oauth_flow_provider.refresh_token(
8993
refresh_token=self._token.refresh_token
@@ -92,30 +96,29 @@ def _refresh_token(self):
9296
def _run_with_attempted_refresh(self, func: Callable[[OAuthToken], T]) -> T:
9397
"""
9498
Attempt to run func, and if it fails with a 401, refresh the token and try again.
95-
9699
If it fails with a 401 again, raise the exception.
97100
"""
98101
try:
99102
return func(self.get_token())
100103
except requests.HTTPError as e:
101-
if e.response is not None and e.response.status_code == 401:
104+
if e.response.status_code == 401:
102105
self._refresh_token()
103106
return func(self.get_token())
104107
else:
105108
raise e
106109

107110
@property
108-
def url(self):
111+
def url(self) -> str:
109112
return remove_prefixes(self._hostname, ["https://", "http://"])
110113

111-
def sign_in(self) -> None:
114+
def sign_in(self) -> str:
112115
self._auth_request = self._server_oauth_flow_provider.generate_auth_request()
113-
webbrowser.open(self._auth_request.url)
116+
return self._auth_request.url
114117

115-
def _start_auto_refresh(self):
116-
def _auto_refresh_token():
118+
def _start_auto_refresh(self) -> None:
119+
def _auto_refresh_token() -> None:
117120
while not self._stop_refresh_event.is_set():
118-
if self._token:
121+
if self._token and self._token.refresh_token:
119122
# Sleep for (expires_in - 60) seconds to refresh the token 1 minute before it expires
120123
time.sleep(self._token.expires_in - 60)
121124
self._token = self._server_oauth_flow_provider.refresh_token(
@@ -129,9 +132,10 @@ def _auto_refresh_token():
129132
refresh_thread.start()
130133

131134
def set_token(self, code: str, state: str) -> None:
132-
if self._auth_request is None or state != self._auth_request.state:
133-
raise RuntimeError("Unable to verify the state")
134-
135+
if not self._auth_request:
136+
raise RuntimeError("Must sign in prior to setting token")
137+
if state != self._auth_request.state:
138+
raise RuntimeError("Unable to verify state")
135139
self._token = self._server_oauth_flow_provider.get_token(
136140
code=code, code_verifier=self._auth_request.code_verifier
137141
)

foundry/_errors/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515

1616
from foundry._errors.environment_not_configured import EnvironmentNotConfigured
17-
from foundry._errors.helpers import format_error_message
1817
from foundry._errors.not_authenticated import NotAuthenticated
1918
from foundry._errors.palantir_rpc_exception import PalantirRPCException
2019
from foundry._errors.sdk_internal_error import SDKInternalError

foundry/_errors/environment_not_configured.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414

1515

1616
class EnvironmentNotConfigured(Exception):
17-
pass
17+
def __init__(self, message: str) -> None:
18+
super().__init__(message)

foundry/_errors/helpers.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

foundry/_errors/palantir_rpc_exception.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@
1313
# limitations under the License.
1414

1515

16+
import json
1617
from typing import Any
1718
from typing import Dict
1819

19-
from foundry._errors.helpers import format_error_message
20+
21+
def format_error_message(fields: Dict[str, Any]) -> str:
22+
return json.dumps(fields, sort_keys=True, indent=4, default=str)
2023

2124

2225
class PalantirRPCException(Exception):
2326
def __init__(self, error_metadata: Dict[str, Any]):
2427
super().__init__(format_error_message(error_metadata))
25-
self.name: str = error_metadata["errorName"]
26-
self.parameters: Dict[str, Any] = error_metadata["parameters"]
27-
self.error_instance_id: str = error_metadata["errorInstanceId"]
28+
self.name = error_metadata.get("errorName")
29+
self.parameters = error_metadata.get("parameters")
30+
self.error_instance_id = error_metadata.get("errorInstanceId")

0 commit comments

Comments
 (0)