diff --git a/.bumpversion.cfg b/.bumpversion.cfg index fb6dc1e4..a9c8428f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.34.0 +current_version = 0.35.0 commit = True tag = False message = chore: Bump version from {current_version} to {new_version} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..ebd054b5 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,42 @@ +# Configuration for Ignoring Revisions in Git Blame +# +# This file contains a list of revisions that are not helpful when assigning blame, such as source +# code reformatting, Python `import` sorting, etc., so that `git blame` is able to ignore those +# commits. +# +# > Ignore changes made by the revision when assigning blame, as if the change never happened. +# > Lines that were changed or added by an ignored commit will be blamed on the previous commit +# > that changed that line or nearby lines. +# +# Documentation: +# - https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt +# - https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view +# +# Using this file with Git Blame: +# - GitHub automatically uses this configuration file. +# - To use locally, run `git config blame.ignoreRevsFile .git-blame-ignore-revs`. + +# Sort Python imports with Isort +33443511137a819050116358d46484a02e1b1157 +2e839bce77b2a497789cd8db0603b59b34dc2a24 +3e65ad14b0f2e35b6b817be668685682b15594a2 + +# Reformat source code using 'Black' +30c98966eb67bb7155085a0801590d4de5ee27d4 +fdad3571586afb8dc94f280572d315434b52b11c +c3453173311d98eb8db9f35067a62bc8a6e296d1 +b5d75137c6c3cffbbc12455f02306134871ca7e8 +df26fa3d114fe063d3809b75931b78b9472adc98 +e4a165f408c9d6a78bc929f379fd5adb5435f824 +d9f7e31f8f9a13e739859d0282e36591cec10ec6 +a98bf81dfef2ddd0a7d72a3b329aee214a020894 +8429acfe04f0cb88ea528d5c580d397db269fab4 +84c1be060c364c7316b1705ef41283b17f1061dc +c5040e9878dbf62cceb6262a1aa27df72d0c0752 +da84feb45852860f549bb982d8f5371e3f5dffa3 + +# Reformat source code +605f6fe7c228401492177c93d5c661a9d2c03c8d + +# Move files without changing them +347201cc5442962a0bd764df28bd4709e5b3fcd6 diff --git a/HISTORY.md b/HISTORY.md index 8a8198b2..a59aa594 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # History +## 0.35.0 (2024-09-26) + +- (PR #706, 2024-09-26) Improvements and fixes related to validation of trusted inputs +- (PR #582, 2024-09-26) Add configuration for ignoring revisions in Git Blame +- (PR #707, 2024-09-26) Make file `setup.py` executable again + ## 0.34.0 (2024-09-26) - (PR #690, 2024-09-25) chore(deps): Bump lxml from 5.2.2 to 5.3.0 diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 diff --git a/src/cl_sii/__init__.py b/src/cl_sii/__init__.py index 6436eb09..ee2f739d 100644 --- a/src/cl_sii/__init__.py +++ b/src/cl_sii/__init__.py @@ -4,4 +4,4 @@ """ -__version__ = '0.34.0' +__version__ = '0.35.0' diff --git a/src/cl_sii/dte/data_models.py b/src/cl_sii/dte/data_models.py index 73f1b9a4..673bf92c 100644 --- a/src/cl_sii/dte/data_models.py +++ b/src/cl_sii/dte/data_models.py @@ -918,3 +918,6 @@ def validate_referencias_codigo_ref_is_consistent_with_tipo_dte(self) -> DteXmlD ) return self + + +DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER = pydantic.TypeAdapter(DteXmlData) diff --git a/src/cl_sii/dte/parse.py b/src/cl_sii/dte/parse.py index 0b5ddf80..fae2912c 100644 --- a/src/cl_sii/dte/parse.py +++ b/src/cl_sii/dte/parse.py @@ -117,7 +117,7 @@ def validate_dte_xml(xml_doc: XmlElement) -> None: xml_utils.validate_xml_doc(DTE_XML_SCHEMA_OBJ, xml_doc) -def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: +def parse_dte_xml(xml_doc: XmlElement, trust_input: bool = False) -> data_models.DteXmlData: """ Parse data from a DTE XML doc. @@ -125,6 +125,16 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: It is assumed that ``xml_doc`` is an ``{http://www.sii.cl/SiiDte}/DTE`` XML element. + :param xml_doc: + DTE XML document. + :param trust_input: + If ``True``, the input data is trusted to be valid and + some validation errors are replaced by warnings. + + .. warning:: + Use this option *only* if the DTE XML document was obtained directly + from the SII *and* you need to work around some validation errors + that the SII should have caught, but let through. :raises ValueError: :raises TypeError: :raises NotImplementedError: @@ -511,23 +521,28 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData: _text_strip_or_raise(signature_key_info_x509_cert_em) ) - return data_models.DteXmlData( - emisor_rut=emisor_rut_value, - tipo_dte=tipo_dte_value, - folio=folio_value, - fecha_emision_date=fecha_emision_value, - receptor_rut=receptor_rut_value, - monto_total=monto_total_value, - emisor_razon_social=emisor_razon_social_value, - receptor_razon_social=receptor_razon_social_value, - fecha_vencimiento_date=fecha_vencimiento_value, - firma_documento_dt=tmst_firma_value, - signature_value=signature_signature_value, - signature_x509_cert_der=signature_key_info_x509_cert_der, - emisor_giro=emisor_giro_value, - emisor_email=emisor_email_value, - receptor_email=receptor_email_value, - referencias=referencia_xml_list, + return data_models.DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.validate_python( + dict( + emisor_rut=emisor_rut_value, + tipo_dte=tipo_dte_value, + folio=folio_value, + fecha_emision_date=fecha_emision_value, + receptor_rut=receptor_rut_value, + monto_total=monto_total_value, + emisor_razon_social=emisor_razon_social_value, + receptor_razon_social=receptor_razon_social_value, + fecha_vencimiento_date=fecha_vencimiento_value, + firma_documento_dt=tmst_firma_value, + signature_value=signature_signature_value, + signature_x509_cert_der=signature_key_info_x509_cert_der, + emisor_giro=emisor_giro_value, + emisor_email=emisor_email_value, + receptor_email=receptor_email_value, + referencias=referencia_xml_list, + ), + context={ + data_models.VALIDATION_CONTEXT_TRUST_INPUT: trust_input, + }, ) diff --git a/src/cl_sii/rtc/data_models_aec.py b/src/cl_sii/rtc/data_models_aec.py index 271af93e..94481e57 100644 --- a/src/cl_sii/rtc/data_models_aec.py +++ b/src/cl_sii/rtc/data_models_aec.py @@ -798,3 +798,6 @@ def validate_signature_value_and_signature_x509_cert_der_may_only_be_none_togeth ) return self + + +AEC_XML_PYDANTIC_TYPE_ADAPTER = pydantic.TypeAdapter(AecXml) diff --git a/src/cl_sii/rtc/parse_aec.py b/src/cl_sii/rtc/parse_aec.py index 54ee4f96..ffbdc65e 100644 --- a/src/cl_sii/rtc/parse_aec.py +++ b/src/cl_sii/rtc/parse_aec.py @@ -26,7 +26,7 @@ import cl_sii.dte.data_models import cl_sii.dte.parse from cl_sii.dte.constants import TipoDte -from cl_sii.dte.data_models import DteXmlData +from cl_sii.dte.data_models import DteXmlData, is_input_trusted_according_to_validation_context from cl_sii.dte.parse import DTE_XMLNS_MAP from cl_sii.libs import encoding_utils, tz_utils, xml_utils from cl_sii.libs.xml_utils import XmlElement @@ -88,7 +88,7 @@ def parse_aec_xml(xml_doc: XmlElement, trust_input: bool = False) -> data_models that the SII should have caught, but let through. """ aec_struct = _Aec.parse_xml(xml_doc, trust_input=trust_input) - return aec_struct.as_aec_xml() + return aec_struct.as_aec_xml(trust_input=trust_input) ############################################################################### @@ -656,10 +656,11 @@ def parse_xml_to_dict(xml_em: XmlElement) -> Mapping[str, object]: @pydantic.field_validator('dte', mode='before') @classmethod - def validate_dte(cls, v: object) -> object: + def validate_dte(cls, v: object, info: pydantic.ValidationInfo) -> object: if isinstance(v, XmlElement): cl_sii.dte.parse.validate_dte_xml(v) - v = cl_sii.dte.parse.parse_dte_xml(v) + trust_dte = is_input_trusted_according_to_validation_context(info.context) + v = cl_sii.dte.parse.parse_dte_xml(v, trust_input=trust_dte) return v # @pydantic.validator('tmst_firma') @@ -909,7 +910,7 @@ def parse_xml(cls, xml_doc: XmlElement, trust_input: bool = False) -> _Aec: }, ) - def as_aec_xml(self) -> data_models_aec.AecXml: + def as_aec_xml(self, trust_input: bool = False) -> data_models_aec.AecXml: doc_aec_struct = self.documento_aec signature_over_doc_aec_struct = self.signature @@ -922,17 +923,22 @@ def as_aec_xml(self) -> data_models_aec.AecXml: cesion_struct.as_cesion_aec_xml() for cesion_struct in cesion_struct_list ] - return data_models_aec.AecXml( - dte=dte, - cedente_rut=caratula_struct.rut_cedente, - cesionario_rut=caratula_struct.rut_cesionario, - fecha_firma_dt=caratula_struct.tmst_firmaenvio, - signature_value=signature_over_doc_aec_struct.signature_value, - signature_x509_cert_der=signature_over_doc_aec_struct.key_info_x509_data_x509_cert, - cesiones=aec_xml_cesion_list, - contacto_nombre=caratula_struct.nmb_contacto, - contacto_telefono=caratula_struct.fono_contacto, - contacto_email=caratula_struct.mail_contacto, + return data_models_aec.AEC_XML_PYDANTIC_TYPE_ADAPTER.validate_python( + dict( + dte=dte, + cedente_rut=caratula_struct.rut_cedente, + cesionario_rut=caratula_struct.rut_cesionario, + fecha_firma_dt=caratula_struct.tmst_firmaenvio, + signature_value=signature_over_doc_aec_struct.signature_value, + signature_x509_cert_der=signature_over_doc_aec_struct.key_info_x509_data_x509_cert, + cesiones=aec_xml_cesion_list, + contacto_nombre=caratula_struct.nmb_contacto, + contacto_telefono=caratula_struct.fono_contacto, + contacto_email=caratula_struct.mail_contacto, + ), + context={ + cl_sii.dte.data_models.VALIDATION_CONTEXT_TRUST_INPUT: trust_input, + }, ) @staticmethod diff --git a/src/tests/test_dte_data_models.py b/src/tests/test_dte_data_models.py index 94f39fd2..d88b48a9 100644 --- a/src/tests/test_dte_data_models.py +++ b/src/tests/test_dte_data_models.py @@ -14,6 +14,7 @@ TipoDte, ) from cl_sii.dte.data_models import ( # noqa: F401 + DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER, VALIDATION_CONTEXT_TRUST_INPUT, DteDataL0, DteDataL1, @@ -1062,8 +1063,6 @@ def setUpClass(cls) -> None: 'test_data/sii-crypto/DTE--96670340-7--61--110616-cert.der' ) - cls.dte_xml_data_pydantic_type_adapter = pydantic.TypeAdapter(DteXmlData) - def setUp(self) -> None: super().setUp() @@ -1788,7 +1787,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_tipo_dte_for_trusted_i ) invalid_but_trusted_obj: Mapping[str, object] = { - **self.dte_xml_data_pydantic_type_adapter.dump_python(obj), + **DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.dump_python(obj), **dict( referencias=[obj_referencia], ), @@ -1797,7 +1796,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_tipo_dte_for_trusted_i try: with self.assertLogs('cl_sii.dte.data_models', level='WARNING') as assert_logs_cm: - self.dte_xml_data_pydantic_type_adapter.validate_python( + DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.validate_python( invalid_but_trusted_obj, context=validation_context ) except pydantic.ValidationError as exc: @@ -1876,7 +1875,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_emisor_rut_for_trusted ) invalid_but_trusted_obj: Mapping[str, object] = { - **self.dte_xml_data_pydantic_type_adapter.dump_python(obj), + **DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.dump_python(obj), **dict( referencias=[obj_referencia], ), @@ -1885,7 +1884,7 @@ def test_validate_referencias_rut_otro_is_consistent_with_emisor_rut_for_trusted try: with self.assertLogs('cl_sii.dte.data_models', level='WARNING') as assert_logs_cm: - self.dte_xml_data_pydantic_type_adapter.validate_python( + DTE_XML_DATA_PYDANTIC_TYPE_ADAPTER.validate_python( invalid_but_trusted_obj, context=validation_context ) except pydantic.ValidationError as exc: