Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1152699
requirements: update dependencies of some packages
glarrain Apr 7, 2020
6994111
requirements: update 'setuptools' (release)
glarrain Apr 7, 2020
3bb9e85
requirements: update 'tox' (test)
glarrain Apr 8, 2020
9c02394
libs.csv_utils: prepare for 'mypy' update
glarrain Apr 8, 2020
836985d
requirements: update 'mypy' (test)
glarrain Apr 8, 2020
0588c88
requirements: update 'flake8' (test)
glarrain Apr 8, 2020
549a1f1
requirements: update 'codecov' (test)
glarrain Apr 8, 2020
f9bad92
libs.crypto_utils: prepare tests for 'cryptography' update
glarrain Apr 8, 2020
b335c6d
requirements: update 'cryptography'
glarrain Apr 8, 2020
add3f78
requirements: update 'jsonschema'
glarrain Apr 8, 2020
995af20
Merge pull request #107 from fyntex/task/requirements/update
glarrain Apr 10, 2020
53868ef
dte.data_models: duplicate `DteDataL2` as `DteXmlData`
glarrain Apr 11, 2020
33b613a
dte.data_models: add `DteXmlData.as_dte_data_l2`
glarrain Apr 11, 2020
7c41e82
dte.data_models: add test cases
glarrain Apr 12, 2020
2cf2fbe
dte: change `parse_dte_xml` return type to `DteXmlData`
glarrain Apr 11, 2020
5c0d53e
Merge pull request #108 from fyntex/feature/dte/data-models/add-dtexm…
glarrain Apr 13, 2020
52c0c0d
rcv.parse_csv: move code from 'fd-cl-data' in here
glarrain Apr 10, 2020
64cc860
Merge pull request #109 from fyntex/task/rcv/parse_csv/move-code
glarrain Apr 13, 2020
9ec4872
rcv.data_models: move some fields to subclasses
glarrain Apr 12, 2020
9a6852d
Merge pull request #110 from fyntex/task/rcv/data_models/move-code
glarrain Apr 13, 2020
b948006
Bump version: 0.9.1 → 0.10.0.a1
glarrain Apr 13, 2020
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
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
Loading