From 7ced65434ba97f8d7a180b7e273555fabd5c6f8f Mon Sep 17 00:00:00 2001 From: Tobias Wochinger Date: Fri, 2 Feb 2024 11:48:40 +0100 Subject: [PATCH] fix: timestamp parsing (#156) * fix: timestamp parsing * tests: fix CI * style: fix docstring --- deepset_cloud_sdk/_api/files.py | 3 +- deepset_cloud_sdk/_api/upload_sessions.py | 9 ++--- deepset_cloud_sdk/_utils/datetime.py | 12 +++++++ tests/unit/api/test_upload_sessions.py | 43 +++++++++++++++++++++++ tests/unit/utils/test_datetime_utils.py | 17 +++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 deepset_cloud_sdk/_utils/datetime.py create mode 100644 tests/unit/utils/test_datetime_utils.py diff --git a/deepset_cloud_sdk/_api/files.py b/deepset_cloud_sdk/_api/files.py index 6fcc1def..a139454d 100644 --- a/deepset_cloud_sdk/_api/files.py +++ b/deepset_cloud_sdk/_api/files.py @@ -18,6 +18,7 @@ from deepset_cloud_sdk._api.deepset_cloud_api import DeepsetCloudAPI from deepset_cloud_sdk._api.upload_sessions import WriteMode +from deepset_cloud_sdk._utils.datetime import from_isoformat logger = structlog.get_logger(__name__) @@ -54,7 +55,7 @@ def from_dict(cls, env: Dict[str, Any]) -> Any: :param env: Dictionary to parse. """ to_parse = {k: v for k, v in env.items() if k in inspect.signature(cls).parameters} - to_parse["created_at"] = datetime.datetime.fromisoformat(to_parse["created_at"]) + to_parse["created_at"] = from_isoformat(to_parse["created_at"]) to_parse["file_id"] = UUID(to_parse["file_id"]) return cls(**to_parse) diff --git a/deepset_cloud_sdk/_api/upload_sessions.py b/deepset_cloud_sdk/_api/upload_sessions.py index 03633e55..0486f766 100644 --- a/deepset_cloud_sdk/_api/upload_sessions.py +++ b/deepset_cloud_sdk/_api/upload_sessions.py @@ -11,6 +11,7 @@ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed from deepset_cloud_sdk._api.deepset_cloud_api import DeepsetCloudAPI +from deepset_cloud_sdk._utils.datetime import from_isoformat from deepset_cloud_sdk.models import UserInfo logger = structlog.get_logger(__name__) @@ -146,7 +147,7 @@ async def create(self, workspace_name: str, write_mode: WriteMode = WriteMode.KE return UploadSession( session_id=UUID(response_body["session_id"]), documentation_url=response_body["documentation_url"], - expires_at=datetime.datetime.fromisoformat(response_body["expires_at"]), + expires_at=from_isoformat(response_body["expires_at"]), aws_prefixed_request_config=AWSPrefixedRequestConfig( fields=response_body["aws_prefixed_request_config"]["fields"], url=response_body["aws_prefixed_request_config"]["url"], @@ -211,7 +212,7 @@ async def status(self, workspace_name: str, session_id: UUID) -> UploadSessionSt return UploadSessionStatus( session_id=UUID(response_body["session_id"]), documentation_url=response_body["documentation_url"], - expires_at=datetime.datetime.fromisoformat(response_body["expires_at"]), + expires_at=from_isoformat(response_body["expires_at"]), ingestion_status=UploadSessionIngestionStatus( failed_files=response_body["ingestion_status"]["failed_files"], finished_files=response_body["ingestion_status"]["finished_files"], @@ -267,8 +268,8 @@ async def list( given_name=upload_session["created_by"]["given_name"], family_name=upload_session["created_by"]["family_name"], ), - expires_at=datetime.datetime.fromisoformat(upload_session["expires_at"]), - created_at=datetime.datetime.fromisoformat(upload_session["created_at"]), + expires_at=from_isoformat(upload_session["expires_at"]), + created_at=from_isoformat(upload_session["created_at"]), write_mode=UploadSessionWriteModeEnum(upload_session["write_mode"]), status=UploadSessionStatusEnum(upload_session["status"]), ) diff --git a/deepset_cloud_sdk/_utils/datetime.py b/deepset_cloud_sdk/_utils/datetime.py new file mode 100644 index 00000000..208ad443 --- /dev/null +++ b/deepset_cloud_sdk/_utils/datetime.py @@ -0,0 +1,12 @@ +"""Utility functions for working with datetime objects.""" +from datetime import datetime + + +def from_isoformat(date_str: str) -> datetime: + """Parse a date string in ISO 8601 format and returns a datetime object. + + Our new Pydantic 2.0 API returns with the `Z` suffix, but the old one returns with `+00:00` + Python versions < 3.12 don't support the `Z` suffix, so we need to replace it with `+00:00` + """ + date_str = date_str.replace("Z", "+00:00") + return datetime.fromisoformat(date_str) diff --git a/tests/unit/api/test_upload_sessions.py b/tests/unit/api/test_upload_sessions.py index 90ac6ecb..b8a8b620 100644 --- a/tests/unit/api/test_upload_sessions.py +++ b/tests/unit/api/test_upload_sessions.py @@ -195,6 +195,49 @@ async def test_list_sessions( assert result.data[0].write_mode == UploadSessionWriteModeEnum.KEEP assert result.data[0].status == UploadSessionStatusEnum.OPEN + async def test_list_sessions_with_z_timestamp( + self, upload_session_client: UploadSessionsAPI, mocked_deepset_cloud_api: Mock + ) -> None: + session_id = uuid4() + timestamp = datetime.datetime.now() + user_id = uuid4() + + mocked_deepset_cloud_api.get.return_value = Response( + status_code=codes.OK, + json={ + "data": [ + { + "session_id": str(session_id), + "created_by": { + "given_name": "Kristof", + "family_name": "Test", + "user_id": str(user_id), + }, + "created_at": timestamp.isoformat().replace("+00:00", "Z"), + "expires_at": timestamp.isoformat().replace("+00:00", "Z"), + "write_mode": "KEEP", + "status": "OPEN", + }, + ], + "has_more": True, + "total": 23, + }, + ) + result: UploadSessionDetailList = await upload_session_client.list( + workspace_name="sdk_read", is_expired=True, limit=1, page_number=10 + ) + assert result.has_more is True + assert result.total == 23 + assert len(result.data) == 1 + assert result.data[0].session_id == session_id + assert result.data[0].created_by.given_name == "Kristof" + assert result.data[0].created_by.family_name == "Test" + assert result.data[0].created_by.user_id == user_id + assert result.data[0].created_at == timestamp + assert result.data[0].expires_at == timestamp + assert result.data[0].write_mode == UploadSessionWriteModeEnum.KEEP + assert result.data[0].status == UploadSessionStatusEnum.OPEN + @pytest.mark.parametrize("first_status_code", [codes.BAD_GATEWAY, codes.INTERNAL_SERVER_ERROR]) async def test_list_sessions_with_retry( self, upload_session_client: UploadSessionsAPI, mocked_deepset_cloud_api: Mock, first_status_code: int diff --git a/tests/unit/utils/test_datetime_utils.py b/tests/unit/utils/test_datetime_utils.py new file mode 100644 index 00000000..c63ce83f --- /dev/null +++ b/tests/unit/utils/test_datetime_utils.py @@ -0,0 +1,17 @@ +from datetime import datetime, timezone + +import pytest + +from deepset_cloud_sdk._utils.datetime import from_isoformat + + +class TestFromIsoformat: + @pytest.mark.parametrize( + "input", + [ + "2024-02-03T08:10:10.335884Z", + "2024-02-03T08:10:10.335884+00:00", + ], + ) + def test_fromisoformat(self, input: str) -> None: + assert from_isoformat(input) == datetime(2024, 2, 3, 8, 10, 10, 335884).replace(tzinfo=timezone.utc)