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
50 changes: 30 additions & 20 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.libs import encoding_utils
from cl_sii.rut import Rut

from . import constants
Expand Down Expand Up @@ -79,9 +80,19 @@ def validate_clean_str(value: str) -> None:
raise ValueError("Value has leading or trailing whitespace characters.", value)


def validate_clean_bytes(value: bytes) -> None:
if len(value.strip()) != len(value):
raise ValueError("Value has leading or trailing whitespace characters.", value)


def validate_non_empty_str(value: str) -> None:
if len(value.strip()) == 0:
raise ValueError("String (stripped) length is 0.")
raise ValueError("String value length (stripped) is 0.")


def validate_non_empty_bytes(value: bytes) -> None:
if len(value.strip()) == 0:
raise ValueError("Bytes value length (stripped) is 0.")


@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -327,14 +338,16 @@ class DteDataL2(DteDataL1):
Datetime on which the "documento" was digitally signed.
"""

signature_value_base64: Optional[str] = dc_field(default=None)
signature_value: Optional[bytes] = dc_field(default=None)
"""
DTE's digital signature's value (as base64 str).
DTE's digital signature's value (raw bytes, without base64 encoding).
"""

signature_x509_cert_base64: Optional[str] = dc_field(default=None)
signature_x509_cert_pem: Optional[bytes] = dc_field(default=None)
"""
DTE's digital signature's X509 certificate (as base64 str).
DTE's digital signature's PEM-encoded X.509 cert.

PEM-encoded implies base64-encoded.
"""

emisor_giro: Optional[str] = dc_field(default=None)
Expand Down Expand Up @@ -377,21 +390,18 @@ def __post_init__(self) -> None:
if not isinstance(self.firma_documento_dt_naive, datetime):
raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.")

if self.signature_value_base64 is not None:
if not isinstance(self.signature_value_base64, str):
raise TypeError("Inappropriate type of 'signature_value_base64'.")
# TODO: validate that it is base64
# TODO: bytes?
validate_clean_str(self.signature_value_base64)
validate_non_empty_str(self.signature_value_base64)

if self.signature_x509_cert_base64 is not None:
if not isinstance(self.signature_x509_cert_base64, str):
raise TypeError("Inappropriate type of 'signature_x509_cert_base64'.")
# TODO: validate that it is base64
# TODO: bytes?
validate_clean_str(self.signature_x509_cert_base64)
validate_non_empty_str(self.signature_x509_cert_base64)
if self.signature_value is not None:
if not isinstance(self.signature_value, bytes):
raise TypeError("Inappropriate type of 'signature_value'.")
validate_clean_bytes(self.signature_value)
validate_non_empty_bytes(self.signature_value)

if self.signature_x509_cert_pem is not None:
if not isinstance(self.signature_x509_cert_pem, bytes):
raise TypeError("Inappropriate type of 'signature_x509_cert_pem'.")
validate_clean_bytes(self.signature_x509_cert_pem)
validate_non_empty_bytes(self.signature_x509_cert_pem)
encoding_utils.validate_base64(self.signature_x509_cert_pem)

if self.emisor_giro is not None:
if not isinstance(self.emisor_giro, str):
Expand Down
20 changes: 15 additions & 5 deletions cl_sii/dte/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@
from datetime import date, datetime
from typing import Tuple

from cl_sii.libs import encoding_utils
from cl_sii.libs import xml_utils
from cl_sii.libs.xml_utils import XmlElement
from cl_sii.libs.xml_utils import XmlElement, XmlElementTree
from cl_sii.rut import Rut
from . import constants
from . import data_models
Expand Down Expand Up @@ -121,12 +122,19 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:
It is assumed that ``xml_doc`` is an
``{http://www.sii.cl/SiiDte}/DTE`` XML element.

:raises ValueError:
:raises TypeError:
:raises NotImplementedError:

"""
# TODO: change response type to a dataclass like 'DteXmlData'.
# TODO: separate the XML parsing stage from the deserialization stage, which could be
# performed by XML-agnostic code (perhaps using Marshmallow or data clacases?).
# See :class:`cl_sii.rcv.parse.RcvCsvRowSchema`.

if not isinstance(xml_doc, (XmlElement, XmlElementTree)):
raise TypeError("'xml_doc' must be an 'XmlElement'.")

xml_em = xml_doc

###########################################################################
Expand Down Expand Up @@ -441,8 +449,10 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:

tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text)

signature_signature_value_base64 = signature_signature_value_em.text.strip()
signature_key_info_x509_cert_base64 = signature_key_info_x509_cert_em.text.strip()
signature_signature_value = encoding_utils.decode_base64_strict(
signature_signature_value_em.text.strip())
signature_key_info_x509_cert_pem = encoding_utils.clean_base64(
signature_key_info_x509_cert_em.text.strip())

return data_models.DteDataL2(
emisor_rut=emisor_rut_value,
Expand All @@ -455,8 +465,8 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:
receptor_razon_social=receptor_razon_social_value,
fecha_vencimiento_date=fecha_vencimiento_value,
firma_documento_dt_naive=tmst_firma_value,
signature_value_base64=signature_signature_value_base64,
signature_x509_cert_base64=signature_key_info_x509_cert_base64,
signature_value=signature_signature_value,
signature_x509_cert_pem=signature_key_info_x509_cert_pem,
emisor_giro=emisor_giro_value,
emisor_email=emisor_email_value,
receptor_email=receptor_email_value,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_dte_data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ def setUp(self) -> None:
receptor_razon_social='MINERA LOS PELAMBRES',
fecha_vencimiento_date=None,
firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40),
signature_value_base64=None,
signature_x509_cert_base64=None,
signature_value=None,
signature_x509_cert_pem=None,
emisor_giro='Ingenieria y Construccion',
emisor_email='hello@example.com',
receptor_email=None,
Expand All @@ -176,8 +176,8 @@ def test_as_dict(self) -> None:
receptor_razon_social='MINERA LOS PELAMBRES',
fecha_vencimiento_date=None,
firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40),
signature_value_base64=None,
signature_x509_cert_base64=None,
signature_value=None,
signature_x509_cert_pem=None,
emisor_giro='Ingenieria y Construccion',
emisor_email='hello@example.com',
receptor_email=None,
Expand Down
Loading