Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add setting disposition_at field for files under retention #710

Merged
merged 7 commits into from Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions boxsdk/object/file.py
@@ -1,7 +1,9 @@
import json
import os
from datetime import datetime
from typing import TYPE_CHECKING, Optional, Tuple, Union, IO, Iterable, List

from boxsdk.util.datetime_formatter import normalize_date_to_rfc3339_format
from .item import Item
from ..util.api_call_decorator import api_call
from ..util.deprecation_decorator import deprecated
Expand Down Expand Up @@ -717,3 +719,17 @@ def copy(
session=self._session,
response_object=response,
)

@api_call
def set_disposition_at(self, date_time: Union[datetime, str]) -> 'File':
"""
Modifies the retention expiration timestamp for the given file. This date can't be shortened once set on a file.

:param date_time:
A datetime str, eg. '2012-12-12T10:53:43-08:00' or datetime.datetime object. If no timezone info provided,
local timezone will be aplied.
:return:
Updated 'File' object
"""
data = {'disposition_at': normalize_date_to_rfc3339_format(date_time)}
return self.update_info(data=data)
21 changes: 21 additions & 0 deletions boxsdk/util/datetime_formatter.py
@@ -0,0 +1,21 @@
from datetime import datetime
from typing import Union

from dateutil import parser


def normalize_date_to_rfc3339_format(date: Union[datetime, str]) -> str:
"""
Normalizes any datetime string or object to rfc3339 format.

:param date: datetime str or datetime object
:return: date-time str in rfc3339 format
"""
if isinstance(date, str):
antusus marked this conversation as resolved.
Show resolved Hide resolved
date = parser.parse(date)

if not isinstance(date, datetime):
raise TypeError(f"Got unsupported type {date.__class__.__name__!r} for date.")

timezone_aware_datetime = date if date.tzinfo is not None else date.astimezone()
return timezone_aware_datetime.isoformat(timespec='seconds')
8 changes: 8 additions & 0 deletions docs/source/boxsdk.util.rst
Expand Up @@ -20,6 +20,14 @@ boxsdk.util.chunked\_uploader module
:undoc-members:
:show-inheritance:

boxsdk.util.datetime\_formatter module
--------------------------------------

.. automodule:: boxsdk.util.datetime_formatter
:members:
:undoc-members:
:show-inheritance:

boxsdk.util.default\_arg\_value module
--------------------------------------

Expand Down
3 changes: 2 additions & 1 deletion setup.py
Expand Up @@ -55,7 +55,8 @@ def main():
'attrs>=17.3.0',
'requests>=2.4.3',
'requests-toolbelt>=0.4.0, <1.0.0',
'wrapt>=1.10.1'
'wrapt>=1.10.1',
'python-dateutil', # To be removed after dropping Python 3.6
]
redis_requires = ['redis>=2.10.3']
jwt_requires = ['pyjwt>=1.3.0', 'cryptography>=3']
Expand Down
17 changes: 17 additions & 0 deletions test/conftest.py
@@ -1,9 +1,11 @@
import datetime
import json
import logging
import sys
from unittest.mock import Mock

import pytest
import pytz

from boxsdk.network.default_network import DefaultNetworkResponse

Expand Down Expand Up @@ -268,3 +270,18 @@ def mock_enterprise_id():
@pytest.fixture(scope='module')
def mock_group_id():
return 'fake-group-99'


@pytest.fixture(scope='module')
def mock_datetime_rfc3339_str():
return '2035-03-04T10:14:24+14:00'


@pytest.fixture(scope='module')
def mock_timezone_aware_datetime_obj():
return datetime.datetime(2035, 3, 4, 10, 14, 24, microsecond=500, tzinfo=pytz.timezone('US/Alaska'))


@pytest.fixture(scope='module')
def mock_timezone_naive_datetime_obj():
return datetime.datetime(2035, 3, 4, 10, 14, 24, microsecond=500)
45 changes: 45 additions & 0 deletions test/unit/object/test_file.py
Expand Up @@ -3,6 +3,8 @@
from unittest.mock import mock_open, patch, Mock

import pytest
from pytest_lazyfixture import lazy_fixture

from boxsdk.config import API
from boxsdk.exception import BoxAPIException
from boxsdk.object.comment import Comment
Expand Down Expand Up @@ -943,3 +945,46 @@ def test_get_thumbnail_representation_not_available(
params={'fields': 'representations'},
)
assert thumb == b''


@pytest.mark.parametrize(
'disposition_at',
(
lazy_fixture('mock_datetime_rfc3339_str'),
"2035-03-04T10:14:24.000+14:00",
"2035/03/04 10:14:24.000+14:00",
lazy_fixture('mock_timezone_aware_datetime_obj'),
)
)
def test_set_diposition_at(
test_file,
mock_box_session,
disposition_at,
mock_datetime_rfc3339_str,
):
expected_url = test_file.get_url()
expected_data = {'disposition_at': mock_datetime_rfc3339_str}

test_file.set_disposition_at(disposition_at)

mock_box_session.put.assert_called_once_with(
expected_url,
data=json.dumps(expected_data),
headers=None,
params=None,
)


@pytest.mark.parametrize(
'disposition_at',
(
None,
Mock()
)
)
def test_raise_exception_when_set_diposition_at_datetime_is_invalid(
test_file,
disposition_at,
):
with pytest.raises(Exception):
test_file.set_disposition_at(disposition_at)
77 changes: 77 additions & 0 deletions test/unit/util/test_datetime_formatter.py
@@ -0,0 +1,77 @@
from unittest.mock import Mock

import datetime
import pytest

from pytest_lazyfixture import lazy_fixture

from boxsdk.util import datetime_formatter


@pytest.mark.parametrize(
"valid_datetime_format",
(
"2035-03-04T10:14:24+14:00",
"2035-03-04T10:14:24-04:00",
lazy_fixture("mock_datetime_rfc3339_str"),
),
)
def test_leave_datetime_string_unchanged_when_rfc3339_formatted_str_provided(
valid_datetime_format,
):
formatted_str = datetime_formatter.normalize_date_to_rfc3339_format(
valid_datetime_format
)
assert formatted_str == valid_datetime_format


@pytest.mark.parametrize(
"other_datetime_format",
(
"2035-03-04T10:14:24.000+14:00",
"2035-03-04 10:14:24.000+14:00",
"2035/03/04 10:14:24.000+14:00",
"2035/03/04T10:14:24+14:00",
"2035/3/4T10:14:24+14:00",
lazy_fixture('mock_timezone_aware_datetime_obj'),
),
)
def test_normalize_date_to_rfc3339_format_timezone_aware_datetime(
other_datetime_format,
mock_datetime_rfc3339_str,
):
formatted_str = datetime_formatter.normalize_date_to_rfc3339_format(
other_datetime_format
)
assert formatted_str == mock_datetime_rfc3339_str


@pytest.mark.parametrize(
"timezone_naive_datetime",
(
"2035-03-04T10:14:24.000",
"2035-03-04T10:14:24",
lazy_fixture('mock_timezone_naive_datetime_obj')
),
)
def test_add_timezone_info_when_timezone_naive_datetime_provided(
timezone_naive_datetime,
mock_timezone_naive_datetime_obj,
):
formatted_str = datetime_formatter.normalize_date_to_rfc3339_format(
timezone_naive_datetime
)

local_timezone = datetime.datetime.now().tzinfo
expected_datetime = mock_timezone_naive_datetime_obj.astimezone(
tz=local_timezone
).isoformat(timespec="seconds")
assert formatted_str == expected_datetime


@pytest.mark.parametrize("inavlid_datetime_object", (None, Mock()))
def test_throw_type_error_when_invalid_datetime_object_provided(
inavlid_datetime_object,
):
with pytest.raises(TypeError):
datetime_formatter.normalize_date_to_rfc3339_format(inavlid_datetime_object)