Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions cl_sii/dte/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import cl_sii.contribuyente.constants
import cl_sii.rut.constants
from cl_sii.base.constants import SII_OFFICIAL_TZ
from cl_sii.libs import tz_utils
from cl_sii.rut import Rut

Expand Down Expand Up @@ -93,13 +94,6 @@ def validate_non_empty_bytes(value: bytes) -> None:
raise ValueError("Bytes value length is 0.")


def validate_correct_tz(value: datetime, tz: tz_utils.PytzTimezone) -> None:
if not tz_utils.dt_is_aware(value):
raise ValueError("Value must be a timezone-aware datetime.", value)
if value.tzinfo.zone != tz.zone: # type: ignore
raise ValueError(f"Timezone of datetime value must be '{tz.zone!s}'.", value)


@dataclasses.dataclass(frozen=True)
class DteNaturalKey:

Expand Down Expand Up @@ -327,7 +321,7 @@ class DteDataL2(DteDataL1):
# constants
###########################################################################

DATETIME_FIELDS_TZ = tz_utils.TZ_CL_SANTIAGO
DATETIME_FIELDS_TZ = SII_OFFICIAL_TZ

###########################################################################
# fields
Expand Down Expand Up @@ -406,7 +400,7 @@ def __post_init__(self) -> None:
if self.firma_documento_dt is not None:
if not isinstance(self.firma_documento_dt, datetime):
raise TypeError("Inappropriate type of 'firma_documento_dt'.")
validate_correct_tz(self.firma_documento_dt, self.DATETIME_FIELDS_TZ)
tz_utils.validate_dt_tz(self.firma_documento_dt, self.DATETIME_FIELDS_TZ)

if self.signature_value is not None:
if not isinstance(self.signature_value, bytes):
Expand Down
59 changes: 51 additions & 8 deletions cl_sii/libs/tz_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@


TZ_UTC = pytz.UTC # type: PytzTimezone
TZ_CL_SANTIAGO = pytz.timezone('America/Santiago') # type: PytzTimezone

# TODO: remove
UTC = TZ_UTC
TIMEZONE_CL_SANTIAGO = TZ_CL_SANTIAGO
_TZ_CL_SANTIAGO: PytzTimezone = pytz.timezone('America/Santiago')


def get_now_tz_aware() -> datetime:
Expand Down Expand Up @@ -66,7 +62,7 @@ def convert_naive_dt_to_tz_aware(dt: datetime, tz: PytzTimezone) -> datetime:
>>> dt_tz_aware_1.isoformat()
'2018-10-23T04:54:13+00:00'

>>> dt_tz_aware_2 = convert_naive_dt_to_tz_aware(dt_naive, TZ_CL_SANTIAGO)
>>> dt_tz_aware_2 = convert_naive_dt_to_tz_aware(dt_naive, _TZ_CL_SANTIAGO)
>>> dt_tz_aware_2
datetime.datetime(2018, 10, 23, 1, 54, 13, tzinfo=<DstTzInfo 'America/Santiago'
-03-1 day, 21:00:00 DST>)
Expand All @@ -82,6 +78,43 @@ def convert_naive_dt_to_tz_aware(dt: datetime, tz: PytzTimezone) -> datetime:
return dt_tz_aware


def convert_tz_aware_dt_to_naive(dt: datetime, tz: PytzTimezone = None) -> datetime:
"""
Convert a timezone-aware datetime object to an offset-naive one.

Default ``tz`` is UTC.

>>> dt_tz_aware = datetime(2018, 10, 1, 2, 30, 0, tzinfo=TZ_UTC)
>>> dt_tz_aware.isoformat()
'2018-10-01T02:30:00+00:00'

>>> dt_naive_utc = convert_tz_aware_dt_to_naive(dt_tz_aware, TZ_UTC)
>>> dt_naive_utc.isoformat()
'2018-10-01T02:30:00'

>>> dt_naive_cl_santiago = convert_tz_aware_dt_to_naive(dt_tz_aware, _TZ_CL_SANTIAGO)
>>> dt_naive_cl_santiago.isoformat()
'2018-09-30T23:30:00'

>>> int((dt_naive_cl_santiago - dt_naive_utc).total_seconds() / 3600)
-3
>>> (dt_naive_cl_santiago.date() - dt_naive_utc.date()).days
-1

:param dt: timezone-aware datetime
:param tz: timezone e.g. ``pytz.timezone('America/Santiago')``
:raises ValueError: if ``dt`` is not timezone-aware

"""
if not dt_is_aware(dt):
raise ValueError("Value must be a timezone-aware datetime object.")

if tz is None:
tz = TZ_UTC
dt_naive = dt.astimezone(tz).replace(tzinfo=None) # type: datetime
return dt_naive


def dt_is_aware(value: datetime) -> bool:
"""
Return whether datetime ``value`` is "aware".
Expand All @@ -91,7 +124,7 @@ def dt_is_aware(value: datetime) -> bool:
False
>>> dt_is_aware(convert_naive_dt_to_tz_aware(dt_naive, TZ_UTC))
True
>>> dt_is_aware(convert_naive_dt_to_tz_aware(dt_naive, TZ_CL_SANTIAGO))
>>> dt_is_aware(convert_naive_dt_to_tz_aware(dt_naive, _TZ_CL_SANTIAGO))
True

"""
Expand All @@ -110,11 +143,21 @@ def dt_is_naive(value: datetime) -> bool:
True
>>> dt_is_naive(convert_naive_dt_to_tz_aware(dt_naive, TZ_UTC))
False
>>> dt_is_naive(convert_naive_dt_to_tz_aware(dt_naive, TZ_CL_SANTIAGO))
>>> dt_is_naive(convert_naive_dt_to_tz_aware(dt_naive, _TZ_CL_SANTIAGO))
False

"""
if not isinstance(value, datetime):
raise TypeError
# source: 'django.utils.timezone.is_naive' @ Django 2.1.7
return value.utcoffset() is None


def validate_dt_tz(value: datetime, tz: PytzTimezone) -> None:
"""
Validate that ``tz`` is the timezone of ``value``.
"""
if not dt_is_aware(value):
raise ValueError("Value must be a timezone-aware datetime object.")
if value.tzinfo.zone != tz.zone: # type: ignore
raise ValueError(f"Timezone of datetime value must be '{tz.zone!s}'.", value)
4 changes: 0 additions & 4 deletions tests/test_dte_data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,3 @@ def test_validate_non_empty_str(self) -> None:
def test_validate_non_empty_bytes(self) -> None:
# TODO: implement for 'validate_non_empty_bytes'
pass

def test_validate_correct_tz(self) -> None:
# TODO: implement for 'validate_correct_tz'
pass
14 changes: 12 additions & 2 deletions tests/test_libs_tz_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import unittest

from cl_sii.libs.tz_utils import ( # noqa: F401
convert_naive_dt_to_tz_aware, dt_is_aware, dt_is_naive, get_now_tz_aware,
PytzTimezone, TZ_CL_SANTIAGO, TZ_UTC,
convert_naive_dt_to_tz_aware, convert_tz_aware_dt_to_naive,
dt_is_aware, dt_is_naive, get_now_tz_aware, validate_dt_tz,
PytzTimezone, TZ_UTC,
)


Expand All @@ -18,6 +19,11 @@ def test_convert_naive_dt_to_tz_aware(self) -> None:
# Reuse doctests/examples in function docstring.
pass

def test_convert_tz_aware_dt_to_naive(self) -> None:
# TODO: implement for 'convert_tz_aware_dt_to_naive'
# Reuse doctests/examples in function docstring.
pass

def test_dt_is_aware(self) -> None:
# TODO: implement for 'dt_is_aware'
# Reuse doctests/examples in function docstring.
Expand All @@ -27,3 +33,7 @@ def test_dt_is_naive(self) -> None:
# TODO: implement for 'dt_is_naive'
# Reuse doctests/examples in function docstring.
pass

def test_validate_dt_tz(self) -> None:
# TODO: implement for 'validate_dt_tz'
pass