dara-core 1.13.0__py3-none-any.whl → 1.14.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dara/core/auth/base.py CHANGED
@@ -18,7 +18,7 @@ limitations under the License.
18
18
  import abc
19
19
  from typing import Any, ClassVar, Dict, Union
20
20
 
21
- from fastapi import Response
21
+ from fastapi import HTTPException, Response
22
22
  from pydantic import BaseModel
23
23
  from typing_extensions import TypedDict
24
24
 
@@ -91,6 +91,15 @@ class BaseAuthConfig(BaseModel, abc.ABC):
91
91
  :param token: encoded token
92
92
  """
93
93
 
94
+ def refresh_token(self, refresh_token: str) -> tuple[str, str]:
95
+ """
96
+ Create a new session token and refresh token from a refresh token.
97
+
98
+ :param refresh_token: encoded refresh token
99
+ :return: new session token, new refresh token
100
+ """
101
+ raise HTTPException(400, f'Auth config {self.__class__.__name__} does not support token refresh')
102
+
94
103
  def revoke_token(self, token: str, response: Response) -> Union[SuccessResponse, RedirectResponse]:
95
104
  """
96
105
  Revoke a session token.
dara/core/auth/routes.py CHANGED
@@ -16,9 +16,10 @@ limitations under the License.
16
16
  """
17
17
 
18
18
  from inspect import iscoroutinefunction
19
+ from typing import Union, cast
19
20
 
20
21
  import jwt
21
- from fastapi import APIRouter, Depends, HTTPException, Request, Response
22
+ from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
22
23
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
23
24
 
24
25
  from dara.core.auth.base import BaseAuthConfig
@@ -103,6 +104,55 @@ async def _revoke_session(response: Response, credentials: HTTPAuthorizationCred
103
104
  raise HTTPException(status_code=400, detail=BAD_REQUEST_ERROR('No auth credentials passed'))
104
105
 
105
106
 
107
+ @auth_router.post('/refresh-token')
108
+ async def handle_refresh_token(
109
+ response: Response,
110
+ dara_refresh_token: Union[str, None] = Cookie(default=None),
111
+ ):
112
+ """
113
+ Given a refresh token, issues a new session token and refresh token cookie.
114
+
115
+ :param response: FastAPI response object
116
+ :param dara_refresh_token: refresh token cookie
117
+ :param settings: env settings object
118
+ """
119
+ if dara_refresh_token is None:
120
+ raise HTTPException(status_code=400, detail=BAD_REQUEST_ERROR('No refresh token provided'))
121
+
122
+ from dara.core.internal.registries import auth_registry
123
+
124
+ auth_config: BaseAuthConfig = auth_registry.get('auth_config')
125
+
126
+ try:
127
+ # Refresh logic up to implementation
128
+ session_token, refresh_token = auth_config.refresh_token(dara_refresh_token)
129
+
130
+ # Using 'Strict' as it is only used for the refresh-token endpoint so cross-site requests are not expected
131
+ response.set_cookie(
132
+ key='dara_refresh_token', value=refresh_token, secure=True, httponly=True, samesite='strict'
133
+ )
134
+ return {'token': session_token}
135
+ except BaseException as e:
136
+ # Regardless of exception type, clear the refresh token cookie
137
+ response.delete_cookie('dara_refresh_token')
138
+ headers = {'set-cookie': response.headers['set-cookie']}
139
+
140
+ # If an explicit HTTPException was raised, re-raise it with the cookie header
141
+ if isinstance(e, HTTPException):
142
+ dev_logger.error('Auth Error', error=e)
143
+ e.headers = headers
144
+ raise e
145
+
146
+ # Explicitly handle expired signature error
147
+ if isinstance(e, jwt.ExpiredSignatureError):
148
+ dev_logger.error('Expired Token Signature', error=e)
149
+ raise HTTPException(status_code=401, detail=EXPIRED_TOKEN_ERROR, headers=headers)
150
+
151
+ # Otherwise show a generic invalid token error
152
+ dev_logger.error('Invalid Token', error=cast(Exception, e))
153
+ raise HTTPException(status_code=401, detail=INVALID_TOKEN_ERROR, headers=headers)
154
+
155
+
106
156
  # Request to retrieve a session token from the backend. The app does this on startup.
107
157
  @auth_router.post('/session')
108
158
  async def _get_session(body: SessionRequestBody):
@@ -182,6 +182,7 @@ class DataVariable(AnyDataVariable):
182
182
  store: CacheStore,
183
183
  filters: Optional[Union[FilterQuery, dict]] = None,
184
184
  pagination: Optional[Pagination] = None,
185
+ format_for_display: bool = False,
185
186
  ) -> Optional[DataFrame]:
186
187
  """
187
188
  Get the value of this DataVariable.
@@ -212,6 +213,12 @@ class DataVariable(AnyDataVariable):
212
213
 
213
214
  if entry.data is not None:
214
215
  filtered_data, count = apply_filters(entry.data, coerce_to_filter_query(filters), pagination)
216
+ if format_for_display and filtered_data is not None:
217
+ filtered_data = filtered_data.copy()
218
+ for col in filtered_data.columns:
219
+ if filtered_data[col].dtype == 'object':
220
+ # We need to convert all values to string to avoid issues with displaying data in the Table component, for example when displaying datetime and number objects in the same column
221
+ filtered_data.loc[:, col] = filtered_data[col].apply(str)
215
222
  data = filtered_data
216
223
  # Store count for given filters and schema
217
224
  await asyncio.gather(
@@ -239,6 +239,7 @@ class DerivedDataVariable(AnyDataVariable, DerivedVariable):
239
239
  store: CacheStore,
240
240
  filters: Optional[Union[FilterQuery, dict]] = None,
241
241
  pagination: Optional[Pagination] = None,
242
+ format_for_display: bool = False,
242
243
  ) -> Union[BaseTask, DataFrame, None]:
243
244
  """
244
245
  Get the filtered data from the underlying derived variable stored under the specified cache_key.
@@ -297,6 +298,12 @@ class DerivedDataVariable(AnyDataVariable, DerivedVariable):
297
298
 
298
299
  # Run the filtering
299
300
  data = await cls._filter_data(data, count_cache_key, data_entry, store, filters, pagination)
301
+ if format_for_display and data is not None:
302
+ data = data.copy()
303
+ for col in data.columns:
304
+ if data[col].dtype == 'object':
305
+ # We need to convert all values to string to avoid issues with displaying data in the Table component, for example when displaying datetime and number objects in the same column
306
+ data.loc[:, col] = data[col].apply(str)
300
307
 
301
308
  return data
302
309
 
@@ -332,6 +332,7 @@ def create_router(config: Configuration):
332
332
  store,
333
333
  body.filters,
334
334
  Pagination(offset=offset, limit=limit, orderBy=order_by, index=index),
335
+ format_for_display=True,
335
336
  )
336
337
  if isinstance(data, BaseTask):
337
338
  await task_mgr.run_task(data, body.ws_channel)
@@ -342,6 +343,7 @@ def create_router(config: Configuration):
342
343
  store,
343
344
  body.filters,
344
345
  Pagination(offset=offset, limit=limit, orderBy=order_by, index=index),
346
+ format_for_display=True,
345
347
  )
346
348
 
347
349
  dev_logger.debug(