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
85 changes: 54 additions & 31 deletions cl_sii/extras/mm_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
(for serializers)

"""
from __future__ import annotations


try:
import marshmallow
except ImportError as exc: # pragma: no cover
raise ImportError("Package 'marshmallow' is required to use this module.") from exc

import datetime
from typing import Optional
from typing import Any, Mapping, Optional

import marshmallow.fields

Expand Down Expand Up @@ -46,13 +49,18 @@ class RutField(marshmallow.fields.Field):

default_error_messages = {
'invalid': 'Not a syntactically valid RUT.',
'type': 'Invalid type.',
}

def _serialize(self, value: Optional[object], attr: str, obj: object) -> Optional[str]:
def _serialize(
self, value: Optional[object], attr: str | None, obj: object, **kwargs: Any
) -> Optional[str]:
validated = self._validated(value)
return validated.canonical if validated is not None else None

def _deserialize(self, value: str, attr: str, data: dict) -> Optional[Rut]:
def _deserialize(
self, value: str, attr: str | None, data: Mapping[str, Any] | None, **kwargs: Any
) -> Optional[Rut]:
return self._validated(value)

def _validated(self, value: Optional[object]) -> Optional[Rut]:
Expand All @@ -61,10 +69,10 @@ def _validated(self, value: Optional[object]) -> Optional[Rut]:
else:
try:
validated = Rut(value, validate_dv=False) # type: ignore
except TypeError:
self.fail('type')
except ValueError:
self.fail('invalid')
except TypeError as exc:
raise self.make_error('type') from exc
except ValueError as exc:
raise self.make_error('invalid') from exc
return validated


Expand All @@ -89,13 +97,18 @@ class TipoDteField(marshmallow.fields.Field):

default_error_messages = {
'invalid': 'Not a valid Tipo DTE.',
'type': 'Invalid type.',
}

def _serialize(self, value: Optional[object], attr: str, obj: object) -> Optional[int]:
def _serialize(
self, value: Optional[object], attr: str | None, obj: object, **kwargs: Any
) -> Optional[int]:
validated: Optional[TipoDte] = self._validated(value)
return validated.value if validated is not None else None

def _deserialize(self, value: object, attr: str, data: dict) -> Optional[TipoDte]:
def _deserialize(
self, value: object, attr: str | None, data: Mapping[str, Any] | None, **kwargs: Any
) -> Optional[TipoDte]:
return self._validated(value)

def _validated(self, value: Optional[object]) -> Optional[TipoDte]:
Expand All @@ -104,21 +117,21 @@ def _validated(self, value: Optional[object]) -> Optional[TipoDte]:
else:
if isinstance(value, bool):
# is value is bool, `isinstance(value, int)` is True and `int(value)` works!
self.fail('type')
raise self.make_error('type')
try:
value = int(value) # type: ignore
except ValueError:
except ValueError as exc:
# `int('x')` raises 'ValueError', not 'TypeError'
self.fail('type')
except TypeError:
raise self.make_error('type') from exc
except TypeError as exc:
# `int(date(2018, 10, 10))` raises 'TypeError', unlike `int('x')`
self.fail('type')
raise self.make_error('type') from exc

try:
validated = TipoDte(value) # type: ignore
except ValueError:
except ValueError as exc:
# TipoDte('x') raises 'ValueError', not 'TypeError'
self.fail('invalid')
raise self.make_error('invalid') from exc
return validated


Expand All @@ -142,13 +155,18 @@ class RcvTipoDoctoField(marshmallow.fields.Field):

default_error_messages = {
'invalid': "Not a valid RCV's Tipo de Documento.",
'type': "Invalid type.",
}

def _serialize(self, value: Optional[object], attr: str, obj: object) -> Optional[int]:
def _serialize(
self, value: Optional[object], attr: str | None, obj: object, **kwargs: Any
) -> Optional[int]:
validated: Optional[RcvTipoDocto] = self._validated(value)
return validated.value if validated is not None else None

def _deserialize(self, value: object, attr: str, data: dict) -> Optional[RcvTipoDocto]:
def _deserialize(
self, value: object, attr: str | None, data: Mapping[str, Any] | None, **kwargs: Any
) -> Optional[RcvTipoDocto]:
return self._validated(value)

def _validated(self, value: Optional[object]) -> Optional[RcvTipoDocto]:
Expand All @@ -157,21 +175,21 @@ def _validated(self, value: Optional[object]) -> Optional[RcvTipoDocto]:
else:
if isinstance(value, bool):
# is value is bool, `isinstance(value, int)` is True and `int(value)` works!
self.fail('type')
raise self.make_error('type')
try:
value = int(value) # type: ignore
except ValueError:
except ValueError as exc:
# `int('x')` raises 'ValueError', not 'TypeError'
self.fail('type')
except TypeError:
raise self.make_error('type') from exc
except TypeError as exc:
# `int(date(2018, 10, 10))` raises 'TypeError', unlike `int('x')`
self.fail('type')
raise self.make_error('type') from exc

try:
validated = RcvTipoDocto(value) # type: ignore
except ValueError:
except ValueError as exc:
# RcvTipoDocto('x') raises 'ValueError', not 'TypeError'
self.fail('invalid')
raise self.make_error('invalid') from exc
return validated


Expand All @@ -186,14 +204,19 @@ class RcvPeriodoTributarioField(marshmallow.fields.Field):

default_error_messages = {
'invalid': "Not a valid RCV Periodo Tributario.",
'type': "Invalid type.",
}
_string_format = '%Y-%m' # Example: '2019-12'

def _serialize(self, value: Optional[object], attr: str, obj: object) -> Optional[str]:
def _serialize(
self, value: Optional[object], attr: str | None, obj: object, **kwargs: Any
) -> Optional[str]:
validated: Optional[RcvPeriodoTributario] = self._validated(value)
return validated.as_date().strftime(self._string_format) if validated is not None else None

def _deserialize(self, value: object, attr: str, data: dict) -> Optional[RcvPeriodoTributario]:
def _deserialize(
self, value: object, attr: str | None, data: Mapping[str, Any] | None, **kwargs: Any
) -> Optional[RcvPeriodoTributario]:
return self._validated(value)

def _validated(self, value: Optional[object]) -> Optional[RcvPeriodoTributario]:
Expand All @@ -203,10 +226,10 @@ def _validated(self, value: Optional[object]) -> Optional[RcvPeriodoTributario]:
try:
value = datetime.datetime.strptime(value, self._string_format) # type: ignore
value = value.date()
except ValueError:
self.fail('invalid')
except TypeError:
self.fail('type')
except ValueError as exc:
raise self.make_error('invalid') from exc
except TypeError as exc:
raise self.make_error('type') from exc

validated = RcvPeriodoTributario.from_date(value) # type: ignore

Expand Down
38 changes: 20 additions & 18 deletions cl_sii/libs/mm_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from datetime import date, datetime
from typing import Any, Optional, Union
from typing import Any, Mapping, Optional, Union

import marshmallow
import marshmallow.fields
Expand All @@ -22,10 +24,6 @@ def validate_no_unexpected_input_fields(
Usage::

class MySchema(marshmallow.Schema):

class Meta:
strict = True

folio = marshmallow.fields.Integer()

@marshmallow.validates_schema(pass_original=True)
Expand All @@ -36,7 +34,7 @@ def validate_schema(self, data: dict, original_data: dict) -> None:
# Original inspiration from
# https://marshmallow.readthedocs.io/en/2.x-line/extending.html#validating-original-input-data
fields_name_or_load_from = {
field.name if field.load_from is None else field.load_from
field.name if field.data_key is None else field.data_key
for field_key, field in schema.fields.items()
}
unexpected_input_fields = set(original_data) - fields_name_or_load_from
Expand Down Expand Up @@ -98,41 +96,45 @@ def __init__(self, format: Optional[str] = None, **kwargs: Any) -> None:
# TODO: for 'marshmallow 3', rename 'dateformat' to 'datetimeformat'.
self.dateformat = format

def _add_to_schema(self, field_name: str, schema: marshmallow.Schema) -> None:
super()._add_to_schema(field_name, schema)
def _bind_to_schema(self, field_name: str, schema: marshmallow.Schema) -> None:
super()._bind_to_schema(field_name, schema)
self.dateformat = self.dateformat or schema.opts.dateformat

def _serialize(self, value: date, attr: str, obj: object) -> Union[str, None]:
def _serialize(
self, value: date, attr: str | None, obj: object, **kwargs: Any
) -> Union[str, None]:
if value is None:
return None
self.dateformat = self.dateformat or self.DEFAULT_FORMAT
format_func = self.DATEFORMAT_SERIALIZATION_FUNCS.get(self.dateformat, None)
if format_func:
try:
date_str = format_func(value)
except (AttributeError, ValueError):
self.fail('format', input=value)
except (AttributeError, ValueError) as exc:
raise self.make_error('format', input=value) from exc
else:
date_str = value.strftime(self.dateformat)

return date_str

def _deserialize(self, value: str, attr: str, data: dict) -> date:
def _deserialize(
self, value: str, attr: str | None, data: Mapping[str, Any] | None, **kwargs: Any
) -> date:
if not value: # Falsy values, e.g. '', None, [] are not valid
self.fail('invalid')
raise self.make_error('invalid')
self.dateformat = self.dateformat or self.DEFAULT_FORMAT
func = self.DATEFORMAT_DESERIALIZATION_FUNCS.get(self.dateformat)
if func:
try:
date_value = func(value) # type: date
except (TypeError, AttributeError, ValueError):
self.fail('invalid')
except (TypeError, AttributeError, ValueError) as exc:
raise self.make_error('invalid') from exc
elif self.dateformat:
try:
date_value = datetime.strptime(value, self.dateformat).date()
except (TypeError, AttributeError, ValueError):
self.fail('invalid')
except (TypeError, AttributeError, ValueError) as exc:
raise self.make_error('invalid') from exc
else:
self.fail('invalid')
raise self.make_error('invalid')

return date_value
26 changes: 1 addition & 25 deletions cl_sii/libs/rows_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,36 +122,12 @@ def rows_mm_deserialization_iterator(
row_data.pop(_field_name, None)

try:
mm_result: marshmallow.UnmarshalResult = row_schema.load(row_data)
deserialized_row_data: dict = mm_result.data
deserialized_row_data: dict = row_schema.load(row_data)
raised_validation_errors: dict = {}
returned_validation_errors: dict = mm_result.errors
except marshmallow.ValidationError as exc:
deserialized_row_data = {}
raised_validation_errors = dict(exc.normalized_messages())
returned_validation_errors = {}

validation_errors = raised_validation_errors
if returned_validation_errors:
if row_schema.strict:
# 'marshmallow.schema.BaseSchema':
# > :param bool strict: If `True`, raise errors if invalid data are passed in
# > instead of failing silently and storing the errors.
logger.error(
"Marshmallow schema is 'strict' but validation errors were returned by "
"method 'load' ('UnmarshalResult.errors') instead of being raised. "
"Errors: %s",
repr(returned_validation_errors),
)
if raised_validation_errors:
logger.fatal(
"Programming error: either returned or raised validation errors "
"(depending on 'strict') but never both. "
"Returned errors: %s. Raised errors: %s",
repr(returned_validation_errors),
repr(raised_validation_errors),
)

validation_errors.update(returned_validation_errors)

yield row_ix, row_data, deserialized_row_data, validation_errors
Loading