Skip to content

Commit

Permalink
Merge pull request #111 from fyntex/develop
Browse files Browse the repository at this point in the history
Release v0.10.0.a1
  • Loading branch information
glarrain committed Apr 13, 2020
2 parents 328fb51 + b948006 commit ed57cca
Show file tree
Hide file tree
Showing 13 changed files with 642 additions and 131 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.9.1
current_version = 0.10.0.a1
commit = True
tag = True

Expand Down
2 changes: 1 addition & 1 deletion cl_sii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""


__version__ = '0.9.1'
__version__ = '0.10.0.a1'
168 changes: 168 additions & 0 deletions cl_sii/dte/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ class DteDataL2(DteDataL1):
"""
DTE data level 2.
Very similar to :class:`DteXmlData` (and a lot of duplicated code,
unfortunately).
About fields
- ``emisor_razon_social``: redundant but required by the DTE XML schema.
- ``receptor_razon_social``: redundant but required by the DTE XML schema.
Expand Down Expand Up @@ -459,3 +462,168 @@ def as_dte_data_l1(self) -> DteDataL1:
fecha_emision_date=self.fecha_emision_date,
receptor_rut=self.receptor_rut,
monto_total=self.monto_total)


@dataclasses.dataclass(frozen=True)
class DteXmlData(DteDataL1):

"""
DTE XML data.
Very similar to :class:`DteDataL2` (and a lot of duplicated code,
unfortunately).
About fields
- ``emisor_razon_social``: redundant but required by the DTE XML schema.
- ``receptor_razon_social``: redundant but required by the DTE XML schema.
- ``fecha_vencimiento`` (date): important for some business logic
but it is not required by the DTE XML schema.
The class instances are immutable.
"""

###########################################################################
# constants
###########################################################################

DATETIME_FIELDS_TZ = SII_OFFICIAL_TZ

###########################################################################
# fields
###########################################################################

emisor_razon_social: str = dc_field()
"""
"Razón social" (legal name) of the "emisor" of the DTE.
"""

receptor_razon_social: str = dc_field()
"""
"Razón social" (legal name) of the "receptor" of the DTE.
"""

fecha_vencimiento_date: Optional[date] = dc_field(default=None)
"""
"Fecha de vencimiento (pago)" of the DTE.
"""

firma_documento_dt: Optional[datetime] = dc_field(default=None)
"""
Datetime on which the "documento" was digitally signed.
"""

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

signature_x509_cert_der: Optional[bytes] = dc_field(default=None)
"""
DTE's digital signature's DER-encoded X.509 cert.
.. seealso::
Functions :func:`cl_sii.libs.crypto_utils.load_der_x509_cert`
and :func:`cl_sii.libs.crypto_utils.x509_cert_der_to_pem`.
"""

emisor_giro: Optional[str] = dc_field(default=None)
"""
"Giro" of the "emisor" of the DTE.
"""

emisor_email: Optional[str] = dc_field(default=None)
"""
Email address of the "emisor" of the DTE.
"""

receptor_email: Optional[str] = dc_field(default=None)
"""
Email address of the "receptor" of the DTE.
"""

def __post_init__(self) -> None:
"""
Run validation automatically after setting the fields values.
:raises TypeError, ValueError:
"""
super().__post_init__()

if not isinstance(self.emisor_razon_social, str):
raise TypeError("Inappropriate type of 'emisor_razon_social'.")
validate_contribuyente_razon_social(self.emisor_razon_social)

if not isinstance(self.receptor_razon_social, str):
raise TypeError("Inappropriate type of 'receptor_razon_social'.")
validate_contribuyente_razon_social(self.receptor_razon_social)

if self.fecha_vencimiento_date is not None:
if not isinstance(self.fecha_vencimiento_date, date):
raise TypeError("Inappropriate type of 'fecha_vencimiento_date'.")

if self.firma_documento_dt is not None:
if not isinstance(self.firma_documento_dt, datetime):
raise TypeError("Inappropriate type of 'firma_documento_dt'.")
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):
raise TypeError("Inappropriate type of 'signature_value'.")
# warning: do NOT strip a bytes value because "strip" implies an ASCII-encoded text,
# which in this case it is not.
validate_non_empty_bytes(self.signature_value)

if self.signature_x509_cert_der is not None:
if not isinstance(self.signature_x509_cert_der, bytes):
raise TypeError("Inappropriate type of 'signature_x509_cert_der'.")
# warning: do NOT strip a bytes value because "strip" implies an ASCII-encoded text,
# which in this case it is not.
validate_non_empty_bytes(self.signature_x509_cert_der)

if self.emisor_giro is not None:
if not isinstance(self.emisor_giro, str):
raise TypeError("Inappropriate type of 'emisor_giro'.")
validate_clean_str(self.emisor_giro)
validate_non_empty_str(self.emisor_giro)

if self.emisor_email is not None:
if not isinstance(self.emisor_email, str):
raise TypeError("Inappropriate type of 'emisor_email'.")
validate_clean_str(self.emisor_email)
validate_non_empty_str(self.emisor_email)

if self.receptor_email is not None:
if not isinstance(self.receptor_email, str):
raise TypeError("Inappropriate type of 'receptor_email'.")
validate_clean_str(self.receptor_email)
validate_non_empty_str(self.receptor_email)

def as_dte_data_l1(self) -> DteDataL1:
return DteDataL1(
emisor_rut=self.emisor_rut,
tipo_dte=self.tipo_dte,
folio=self.folio,
fecha_emision_date=self.fecha_emision_date,
receptor_rut=self.receptor_rut,
monto_total=self.monto_total)

def as_dte_data_l2(self) -> DteDataL2:
return DteDataL2(
emisor_rut=self.emisor_rut,
tipo_dte=self.tipo_dte,
folio=self.folio,
fecha_emision_date=self.fecha_emision_date,
receptor_rut=self.receptor_rut,
monto_total=self.monto_total,
emisor_razon_social=self.emisor_razon_social,
receptor_razon_social=self.receptor_razon_social,
fecha_vencimiento_date=self.fecha_vencimiento_date,
firma_documento_dt=self.firma_documento_dt,
signature_value=self.signature_value,
signature_x509_cert_der=self.signature_x509_cert_der,
emisor_giro=self.emisor_giro,
emisor_email=self.emisor_email,
receptor_email=self.receptor_email,
)
10 changes: 4 additions & 6 deletions cl_sii/dte/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
>>> parse.clean_dte_xml(xml_doc)
True
>>> parse.validate_dte_xml(xml_doc)
>>> dte_struct = parse.parse_dte_xml(xml_doc)
>>> dte_xml_data = parse.parse_dte_xml(xml_doc)
"""
import io
Expand Down Expand Up @@ -114,8 +114,7 @@ def validate_dte_xml(xml_doc: XmlElement) -> None:
xml_utils.validate_xml_doc(DTE_XML_SCHEMA_OBJ, xml_doc)


# TODO: rename to 'parse_dte_xml_data'
def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:
def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData:
"""
Parse data from a DTE XML doc.
Expand All @@ -128,7 +127,6 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:
: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_csv.RcvVentaCsvRowSchema`.
Expand Down Expand Up @@ -455,14 +453,14 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2:

tmst_firma_value = tz_utils.convert_naive_dt_to_tz_aware(
dt=datetime.fromisoformat(_text_strip_or_raise(tmst_firma_em)),
tz=data_models.DteDataL2.DATETIME_FIELDS_TZ)
tz=data_models.DteXmlData.DATETIME_FIELDS_TZ)

signature_signature_value = encoding_utils.decode_base64_strict(
_text_strip_or_raise(signature_signature_value_em))
signature_key_info_x509_cert_der = encoding_utils.decode_base64_strict(
_text_strip_or_raise(signature_key_info_x509_cert_em))

return data_models.DteDataL2(
return data_models.DteXmlData(
emisor_rut=emisor_rut_value,
tipo_dte=tipo_dte_value,
folio=folio_value,
Expand Down
13 changes: 5 additions & 8 deletions cl_sii/libs/csv_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,7 @@ def create_csv_dict_reader(
:return: a CSV DictReader
"""
# note: mypy wrongly complains: it does not accept 'fieldnames' to be None but that value
# is completely acceptable, and it even is the default!
# > error: Argument "fieldnames" to "DictReader" has incompatible type "None"; expected
# > "Sequence[str]"
# note: mypy wrongly complains:
# > Argument "dialect" to "DictReader" has incompatible type "Type[Dialect]";
# > expected "Union[str, Dialect]"
csv_reader = csv.DictReader( # type: ignore
csv_reader = csv.DictReader(
text_stream,
fieldnames=None, # the values of the first row will be used as the fieldnames
restkey=row_dict_extra_fields_key,
Expand All @@ -38,6 +31,10 @@ def create_csv_dict_reader(

if expected_fields_strict:
if expected_field_names:
if csv_reader.fieldnames is None:
raise Exception(
"Programming error: when a 'csv.DictReader' instance is created with"
"'fieldnames=None', the attribute will be set to the values of the first row.")
if tuple(csv_reader.fieldnames) != expected_field_names:
raise ValueError(
"CSV file field names do not match those expected, or their order.",
Expand Down

0 comments on commit ed57cca

Please sign in to comment.