Repository: planetlabs/planet-client-python
Pull request #1016 (2023-11-22 20:41:44)

Bugfix datetime serialization utc zero offset
**Proposed Changes:**

For inclusion in changelog (if applicable):

1.

Not intended for changelog:

1. Change how timezone aware datetimes with UTC tz are converted to datetime string

**Diff of User Interface**

Old behavior:
passing a timezone aware datetime from the UTC zone to some of the request constructors resulted in malformed datetime strings, e.g: `2022-06-01T01:00:00+00:00Z` - i.e. strings that have BOTH `+00:00` and `Z` at the end. 

At the root of it was `value.utcoffset()` >> `timedelta(hours=0)` evaluating as False even though there is a defined offset here:
```
def _datetime_to_rfc3339(value: datetime) -> str:
    """Converts the datetime to an RFC3339 string"""
    iso = value.isoformat()
    if not value.utcoffset():
        # rfc3339 needs a Z if there is no timezone offset
        iso += 'Z'
    return iso
```

New behavior:
The Z is tagged on only if the utcoffset() is None.

When the TZ is UTC the dt string is:
 `2022-06-01T01:00:00+00:00` 


The previous test setup for timezone aware datetimes obfuscated this bug, so I changed the tests as well.



**PR Checklist:**

- [x] This PR is as small and focused as possible
- [x] If this PR includes proposed changes for inclusion in the changelog, the title of this PR summarizes those changes and is ready for inclusion in the Changelog.
- [x] I have updated docstrings for function changes and docs in the 'docs' folder for user interface / behavior changes
- [x] This PR does not break any examples or I have updated them

**(Optional) @mentions for Notifications:**



Patch:
diff --git a/planet/data_filter.py b/planet/data_filter.py
index 86db69a..6bc4486 100644
--- a/planet/data_filter.py
+++ b/planet/data_filter.py
@@ -149,7 +149,7 @@ def date_range_filter(field_name: str,
 def _datetime_to_rfc3339(value: datetime) -> str:
     """Converts the datetime to an RFC3339 string"""
     iso = value.isoformat()
-    if not value.utcoffset():
+    if value.utcoffset() is None:
         # rfc3339 needs a Z if there is no timezone offset
         iso += 'Z'
     return iso
diff --git a/planet/subscription_request.py b/planet/subscription_request.py
index e6c179d..574a4ed 100644
--- a/planet/subscription_request.py
+++ b/planet/subscription_request.py
@@ -358,7 +358,7 @@ def planetary_variable_source(
 def _datetime_to_rfc3339(value: datetime) -> str:
     """Converts the datetime to an RFC3339 string"""
     iso = value.isoformat()
-    if not value.utcoffset():
+    if value.utcoffset() is None:
         # rfc3339 needs a Z if there is no timezone offset
         iso += 'Z'
     return iso


Test patch:
diff --git a/tests/unit/test_data_filter.py b/tests/unit/test_data_filter.py
index 4e173a2..e782dc0 100644
--- a/tests/unit/test_data_filter.py
+++ b/tests/unit/test_data_filter.py
@@ -12,7 +12,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-from datetime import datetime, timedelta, tzinfo
+from datetime import datetime, timedelta, timezone
 import logging
 import pytest
 
@@ -98,24 +98,16 @@ def test__range_filter_no_conditionals():
                                   callback=_test_callback)
 
 
-class TZTest(tzinfo):
-
-    def __init__(self, offset=None):
-        self.offset = offset
-        super().__init__()
-
-    def utcoffset(self, dt):
-        return timedelta(hours=self.offset) if self.offset else None
-
-
 @pytest.mark.parametrize(
     "dtime,expected",
     [(datetime(2022, 5, 1, 1, 0, 0, 1), '2022-05-01T01:00:00.000001Z'),
      (datetime(2022, 5, 1, 1, 0, 1), '2022-05-01T01:00:01Z'),
      (datetime(2022, 6, 1, 1, 1), '2022-06-01T01:01:00Z'),
      (datetime(2022, 6, 1, 1), '2022-06-01T01:00:00Z'),
-     (datetime(2022, 6, 1, 1, tzinfo=TZTest(0)), '2022-06-01T01:00:00Z'),
-     (datetime(2022, 6, 1, 1, tzinfo=TZTest(1)), '2022-06-01T01:00:00+01:00')])
+     (datetime(2022, 6, 1, 1, tzinfo=timezone(timedelta(hours=1))),
+      '2022-06-01T01:00:00+01:00'),
+     (datetime(2022, 6, 1, 1, tzinfo=timezone(timedelta(0))),
+      '2022-06-01T01:00:00+00:00')])
 def test__datetime_to_rfc3339_basic(dtime, expected):
     assert data_filter._datetime_to_rfc3339(dtime) == expected