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

Feature/datetime conversion fix #179

Merged
merged 11 commits into from Nov 7, 2022
39 changes: 27 additions & 12 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Expand Up @@ -30,6 +30,7 @@ opentelemetry-api = { version = "^1.11.1", optional = true }
opentelemetry-sdk = { version = "^1.11.1", optional = true }
protobuf = "~4.21"
python = "^3.7"
python-dateutil = { version = "^2.8.2", python = "<3.11" }
types-protobuf = "~3.20"
typing-extensions = "^4.2.0"

Expand Down Expand Up @@ -121,7 +122,7 @@ ignore_missing_imports = true
exclude = [
# Ignore generated code
'temporalio/api',
'temporalio/bridge/proto',
'temporalio/bridge/proto'
]

[tool.pydocstyle]
Expand Down
20 changes: 19 additions & 1 deletion temporalio/converter.py
Expand Up @@ -14,6 +14,7 @@
from enum import IntEnum
from typing import (
Any,
Callable,
Dict,
List,
Mapping,
Expand All @@ -34,6 +35,9 @@
import temporalio.api.common.v1
import temporalio.common

if sys.version_info < (3, 11):
# Python's datetime.fromisoformat doesn't support certain formats pre-3.11
from dateutil import parser # type: ignore
# StrEnum is available in 3.11+
if sys.version_info >= (3, 11):
from enum import StrEnum
Expand Down Expand Up @@ -682,6 +686,19 @@ def encode_search_attribute_values(
return default().payload_converter.to_payloads([safe_vals])[0]


def _get_iso_datetime_parser() -> Callable[[str], datetime]:
"""Isolates system version check and returns relevant datetime passer

Returns:
A callable to parse date strings into datetimes.
"""
if sys.version_info >= (3, 11):
return datetime.fromisoformat # noqa
else:
# Isolate import for py > 3.11, as dependency only installed for < 3.11
return parser.isoparse


def decode_search_attributes(
api: temporalio.api.common.v1.SearchAttributes,
) -> temporalio.common.SearchAttributes:
Expand All @@ -702,7 +719,8 @@ def decode_search_attributes(
val = [val]
# Convert each item to datetime if necessary
if v.metadata.get("type") == b"Datetime":
val = [datetime.fromisoformat(v) for v in val]
parser = _get_iso_datetime_parser()
val = [parser(v) for v in val]
ret[k] = val
return ret

Expand Down
39 changes: 38 additions & 1 deletion tests/test_converter.py
Expand Up @@ -4,7 +4,7 @@
import sys
from collections import deque
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum, IntEnum
from typing import (
Any,
Expand Down Expand Up @@ -174,6 +174,43 @@ def test_encode_search_attribute_values():
temporalio.converter.encode_search_attribute_values(["foo", 123])


def test_decode_search_attributes():
"""Tests decode from protobuf for python types"""

def payload(key, dtype, data, encoding=None):
if encoding is None:
encoding = {"encoding": b"json/plain"}
check = temporalio.api.common.v1.Payload(
data=bytes(data, encoding="utf-8"),
metadata={"type": bytes(dtype, encoding="utf-8"), **encoding},
)
return temporalio.api.common.v1.SearchAttributes(indexed_fields={key: check})

# Check basic keyword parsing works
kw_check = temporalio.converter.decode_search_attributes(
payload("kw", "Keyword", '"test-id"')
)
assert kw_check["kw"][0] == "test-id"

# Ensure original DT functionality works
dt_check = temporalio.converter.decode_search_attributes(
payload("dt", "Datetime", '"2020-01-01T00:00:00"')
)
assert dt_check["dt"][0] == datetime(2020, 1, 1, 0, 0, 0)

# Check timezone aware works as server is using ISO 8601
dttz_check = temporalio.converter.decode_search_attributes(
payload("dt", "Datetime", '"2020-01-01T00:00:00Z"')
)
assert dttz_check["dt"][0] == datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)

# Check timezone aware, hour offset
dttz_check = temporalio.converter.decode_search_attributes(
payload("dt", "Datetime", '"2020-01-01T00:00:00+00:00"')
)
assert dttz_check["dt"][0] == datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc)


NewIntType = NewType("NewIntType", int)
MyDataClassAlias = MyDataClass

Expand Down