Skip to content

Commit 91b1373

Browse files
authored
feat: Add setting disposition_at field for files under retention (#710)
Closes: SDK-1921
1 parent 426b2c5 commit 91b1373

File tree

7 files changed

+186
-1
lines changed

7 files changed

+186
-1
lines changed

boxsdk/object/file.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import json
22
import os
3+
from datetime import datetime
34
from typing import TYPE_CHECKING, Optional, Tuple, Union, IO, Iterable, List
45

6+
from boxsdk.util.datetime_formatter import normalize_date_to_rfc3339_format
57
from .item import Item
68
from ..util.api_call_decorator import api_call
79
from ..util.deprecation_decorator import deprecated
@@ -717,3 +719,17 @@ def copy(
717719
session=self._session,
718720
response_object=response,
719721
)
722+
723+
@api_call
724+
def set_disposition_at(self, date_time: Union[datetime, str]) -> 'File':
725+
"""
726+
Modifies the retention expiration timestamp for the given file. This date can't be shortened once set on a file.
727+
728+
:param date_time:
729+
A datetime str, eg. '2012-12-12T10:53:43-08:00' or datetime.datetime object. If no timezone info provided,
730+
local timezone will be aplied.
731+
:return:
732+
Updated 'File' object
733+
"""
734+
data = {'disposition_at': normalize_date_to_rfc3339_format(date_time)}
735+
return self.update_info(data=data)

boxsdk/util/datetime_formatter.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from datetime import datetime
2+
from typing import Union
3+
4+
from dateutil import parser
5+
6+
7+
def normalize_date_to_rfc3339_format(date: Union[datetime, str]) -> str:
8+
"""
9+
Normalizes any datetime string or object to rfc3339 format.
10+
11+
:param date: datetime str or datetime object
12+
:return: date-time str in rfc3339 format
13+
"""
14+
if isinstance(date, str):
15+
date = parser.parse(date)
16+
17+
if not isinstance(date, datetime):
18+
raise TypeError(f"Got unsupported type {date.__class__.__name__!r} for date.")
19+
20+
timezone_aware_datetime = date if date.tzinfo is not None else date.astimezone()
21+
return timezone_aware_datetime.isoformat(timespec='seconds')

docs/source/boxsdk.util.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ boxsdk.util.chunked\_uploader module
2020
:undoc-members:
2121
:show-inheritance:
2222

23+
boxsdk.util.datetime\_formatter module
24+
--------------------------------------
25+
26+
.. automodule:: boxsdk.util.datetime_formatter
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
30+
2331
boxsdk.util.default\_arg\_value module
2432
--------------------------------------
2533

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ def main():
5555
'attrs>=17.3.0',
5656
'requests>=2.4.3',
5757
'requests-toolbelt>=0.4.0, <1.0.0',
58-
'wrapt>=1.10.1'
58+
'wrapt>=1.10.1',
59+
'python-dateutil', # To be removed after dropping Python 3.6
5960
]
6061
redis_requires = ['redis>=2.10.3']
6162
jwt_requires = ['pyjwt>=1.3.0', 'cryptography>=3']

test/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import datetime
12
import json
23
import logging
34
import sys
45
from unittest.mock import Mock
56

67
import pytest
8+
import pytz
79

810
from boxsdk.network.default_network import DefaultNetworkResponse
911

@@ -268,3 +270,18 @@ def mock_enterprise_id():
268270
@pytest.fixture(scope='module')
269271
def mock_group_id():
270272
return 'fake-group-99'
273+
274+
275+
@pytest.fixture(scope='module')
276+
def mock_datetime_rfc3339_str():
277+
return '2035-03-04T10:14:24+14:00'
278+
279+
280+
@pytest.fixture(scope='module')
281+
def mock_timezone_aware_datetime_obj():
282+
return datetime.datetime(2035, 3, 4, 10, 14, 24, microsecond=500, tzinfo=pytz.timezone('US/Alaska'))
283+
284+
285+
@pytest.fixture(scope='module')
286+
def mock_timezone_naive_datetime_obj():
287+
return datetime.datetime(2035, 3, 4, 10, 14, 24, microsecond=500)

test/unit/object/test_file.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from unittest.mock import mock_open, patch, Mock
44

55
import pytest
6+
from pytest_lazyfixture import lazy_fixture
7+
68
from boxsdk.config import API
79
from boxsdk.exception import BoxAPIException
810
from boxsdk.object.comment import Comment
@@ -943,3 +945,46 @@ def test_get_thumbnail_representation_not_available(
943945
params={'fields': 'representations'},
944946
)
945947
assert thumb == b''
948+
949+
950+
@pytest.mark.parametrize(
951+
'disposition_at',
952+
(
953+
lazy_fixture('mock_datetime_rfc3339_str'),
954+
"2035-03-04T10:14:24.000+14:00",
955+
"2035/03/04 10:14:24.000+14:00",
956+
lazy_fixture('mock_timezone_aware_datetime_obj'),
957+
)
958+
)
959+
def test_set_diposition_at(
960+
test_file,
961+
mock_box_session,
962+
disposition_at,
963+
mock_datetime_rfc3339_str,
964+
):
965+
expected_url = test_file.get_url()
966+
expected_data = {'disposition_at': mock_datetime_rfc3339_str}
967+
968+
test_file.set_disposition_at(disposition_at)
969+
970+
mock_box_session.put.assert_called_once_with(
971+
expected_url,
972+
data=json.dumps(expected_data),
973+
headers=None,
974+
params=None,
975+
)
976+
977+
978+
@pytest.mark.parametrize(
979+
'disposition_at',
980+
(
981+
None,
982+
Mock()
983+
)
984+
)
985+
def test_raise_exception_when_set_diposition_at_datetime_is_invalid(
986+
test_file,
987+
disposition_at,
988+
):
989+
with pytest.raises(Exception):
990+
test_file.set_disposition_at(disposition_at)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from unittest.mock import Mock
2+
3+
import datetime
4+
import pytest
5+
6+
from pytest_lazyfixture import lazy_fixture
7+
8+
from boxsdk.util import datetime_formatter
9+
10+
11+
@pytest.mark.parametrize(
12+
"valid_datetime_format",
13+
(
14+
"2035-03-04T10:14:24+14:00",
15+
"2035-03-04T10:14:24-04:00",
16+
lazy_fixture("mock_datetime_rfc3339_str"),
17+
),
18+
)
19+
def test_leave_datetime_string_unchanged_when_rfc3339_formatted_str_provided(
20+
valid_datetime_format,
21+
):
22+
formatted_str = datetime_formatter.normalize_date_to_rfc3339_format(
23+
valid_datetime_format
24+
)
25+
assert formatted_str == valid_datetime_format
26+
27+
28+
@pytest.mark.parametrize(
29+
"other_datetime_format",
30+
(
31+
"2035-03-04T10:14:24.000+14:00",
32+
"2035-03-04 10:14:24.000+14:00",
33+
"2035/03/04 10:14:24.000+14:00",
34+
"2035/03/04T10:14:24+14:00",
35+
"2035/3/4T10:14:24+14:00",
36+
lazy_fixture('mock_timezone_aware_datetime_obj'),
37+
),
38+
)
39+
def test_normalize_date_to_rfc3339_format_timezone_aware_datetime(
40+
other_datetime_format,
41+
mock_datetime_rfc3339_str,
42+
):
43+
formatted_str = datetime_formatter.normalize_date_to_rfc3339_format(
44+
other_datetime_format
45+
)
46+
assert formatted_str == mock_datetime_rfc3339_str
47+
48+
49+
@pytest.mark.parametrize(
50+
"timezone_naive_datetime",
51+
(
52+
"2035-03-04T10:14:24.000",
53+
"2035-03-04T10:14:24",
54+
lazy_fixture('mock_timezone_naive_datetime_obj')
55+
),
56+
)
57+
def test_add_timezone_info_when_timezone_naive_datetime_provided(
58+
timezone_naive_datetime,
59+
mock_timezone_naive_datetime_obj,
60+
):
61+
formatted_str = datetime_formatter.normalize_date_to_rfc3339_format(
62+
timezone_naive_datetime
63+
)
64+
65+
local_timezone = datetime.datetime.now().tzinfo
66+
expected_datetime = mock_timezone_naive_datetime_obj.astimezone(
67+
tz=local_timezone
68+
).isoformat(timespec="seconds")
69+
assert formatted_str == expected_datetime
70+
71+
72+
@pytest.mark.parametrize("inavlid_datetime_object", (None, Mock()))
73+
def test_throw_type_error_when_invalid_datetime_object_provided(
74+
inavlid_datetime_object,
75+
):
76+
with pytest.raises(TypeError):
77+
datetime_formatter.normalize_date_to_rfc3339_format(inavlid_datetime_object)

0 commit comments

Comments
 (0)