Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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.33.0
current_version = 0.34.0
commit = True
tag = False
message = chore: Bump version from {current_version} to {new_version}
Expand Down
8 changes: 8 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# History

## 0.34.0 (2024-09-26)

- (PR #690, 2024-09-25) chore(deps): Bump lxml from 5.2.2 to 5.3.0
- (PR #691, 2024-09-25) chore(deps): Bump marshmallow from 3.21.3 to 3.22.0
- (PR #701, 2024-09-25) Enable type checking for `setuptools`
- (PR #702, 2024-09-25) Enable type checking for `lxml`
- (PR #703, 2024-09-26) Relax some validations for trusted inputs

## 0.33.0 (2024-09-24)

- (PR #689, 2024-09-24) chore(deps): Bump pydantic from 2.7.2 to 2.8.2
Expand Down
6 changes: 0 additions & 6 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,9 @@ ignore_missing_imports = True
[mypy-django_filters.*]
ignore_missing_imports = True

[mypy-lxml.*]
ignore_missing_imports = True

[mypy-rest_framework.*]
ignore_missing_imports = True

[mypy-setuptools.*]
ignore_missing_imports = True

[pydantic-mypy]
init_forbid_extra = True
init_typed = True
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pip-tools==7.4.1
tox==4.20.0
twine==5.1.1
types-jsonschema==4.23.0.20240813
types-lxml==2024.9.16
types-pyOpenSSL==24.1.0.20240722
types-pytz==2024.2.0.20240913
wheel==0.44.0
9 changes: 9 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ cryptography==43.0.1
# -c requirements.txt
# secretstorage
# types-pyopenssl
cssselect==1.2.0
# via types-lxml
distlib==0.3.7
# via virtualenv
docutils==0.19
Expand Down Expand Up @@ -157,10 +159,16 @@ tox==4.20.0
# via -r requirements-dev.in
twine==5.1.1
# via -r requirements-dev.in
types-beautifulsoup4==4.12.0.20240907
# via types-lxml
types-cffi==1.16.0.20240331
# via types-pyopenssl
types-html5lib==1.1.11.20240806
# via types-beautifulsoup4
types-jsonschema==4.23.0.20240813
# via -r requirements-dev.in
types-lxml==2024.9.16
# via -r requirements-dev.in
types-pyopenssl==24.1.0.20240722
# via -r requirements-dev.in
types-pytz==2024.2.0.20240913
Expand All @@ -173,6 +181,7 @@ typing-extensions==4.12.2
# black
# mypy
# rich
# types-lxml
urllib3==1.26.19
# via
# requests
Expand Down
4 changes: 2 additions & 2 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Django>=2.2.24
djangorestframework>=3.10.3,<3.16
importlib-metadata==8.4.0
jsonschema==4.23.0
lxml==5.2.2
marshmallow==3.21.3
lxml==5.3.0
marshmallow==3.22.0
pydantic==2.9.2
pyOpenSSL==24.2.1
pytz==2024.1
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ jsonschema==4.23.0
# via -r requirements.in
jsonschema-specifications==2023.12.1
# via jsonschema
lxml==5.2.2
lxml==5.3.0
# via
# -r requirements.in
# signxml
marshmallow==3.21.3
marshmallow==3.22.0
# via -r requirements.in
packaging==24.1
# via marshmallow
Expand Down
2 changes: 1 addition & 1 deletion src/cl_sii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

"""

__version__ = '0.33.0'
__version__ = '0.34.0'
61 changes: 55 additions & 6 deletions src/cl_sii/dte/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from __future__ import annotations

import dataclasses
import logging
from datetime import date, datetime
from typing import Mapping, Optional, Sequence

Expand All @@ -33,6 +34,9 @@
from .constants import CodigoReferencia, TipoDte


logger = logging.getLogger(__name__)


def validate_dte_folio(value: int) -> None:
"""
Validate value for DTE field ``folio``.
Expand Down Expand Up @@ -99,6 +103,39 @@ def validate_non_empty_bytes(value: bytes) -> None:
raise ValueError("Bytes value length is 0.")


VALIDATION_CONTEXT_TRUST_INPUT: str = 'trust_input'
"""
Key for the validation context to indicate that the input data is trusted.
"""


def is_input_trusted_according_to_validation_context(
validation_context: Optional[Mapping[str, object]]
) -> bool:
"""
Return whether the input data is trusted according to the validation context.

:param validation_context:
The validation context of a Pydantic model.
Get it from ``pydantic.ValidationInfo.context``.

Example for data classes:

>>> dte_xml_data_instance_kwargs: Mapping[str, object] = dict(
... emisor_rut=Rut('60910000-1'), # ...
... )
>>> dte_xml_data_adapter = pydantic.TypeAdapter(DteXmlData)
>>> dte_xml_data_instance: DteXmlData = dte_xml_data_adapter.validate_python(
... dte_xml_data_instance_kwargs,
... context={VALIDATION_CONTEXT_TRUST_INPUT: True}
... )
"""
if validation_context is None:
return False
else:
return validation_context.get(VALIDATION_CONTEXT_TRUST_INPUT) is True


@pydantic.dataclasses.dataclass(
frozen=True,
config=pydantic.ConfigDict(
Expand Down Expand Up @@ -815,7 +852,9 @@ def validate_referencias_numero_linea_ref_order(cls, v: object) -> object:
return v

@pydantic.model_validator(mode='after')
def validate_referencias_rut_otro_is_consistent_with_tipo_dte(self) -> DteXmlData:
def validate_referencias_rut_otro_is_consistent_with_tipo_dte(
self, info: pydantic.ValidationInfo
) -> DteXmlData:
referencias = self.referencias
tipo_dte = self.tipo_dte

Expand All @@ -826,27 +865,37 @@ def validate_referencias_rut_otro_is_consistent_with_tipo_dte(self) -> DteXmlDat
):
for referencia in referencias:
if referencia.rut_otro:
raise ValueError(
message: str = (
f"Setting a 'rut_otro' is not a valid option for this 'tipo_dte':"
f" 'tipo_dte' == {tipo_dte!r},"
f" 'Referencia' number {referencia.numero_linea_ref}.",
f" 'Referencia' number {referencia.numero_linea_ref}."
)
if is_input_trusted_according_to_validation_context(info.context):
logger.warning('Validation failed but input is trusted: %s', message)
else:
raise ValueError(message)

return self

@pydantic.model_validator(mode='after')
def validate_referencias_rut_otro_is_consistent_with_emisor_rut(self) -> DteXmlData:
def validate_referencias_rut_otro_is_consistent_with_emisor_rut(
self, info: pydantic.ValidationInfo
) -> DteXmlData:
referencias = self.referencias
emisor_rut = self.emisor_rut

if isinstance(referencias, Sequence) and isinstance(emisor_rut, Rut):
for referencia in referencias:
if referencia.rut_otro and referencia.rut_otro == emisor_rut:
raise ValueError(
message: str = (
f"'rut_otro' must be different from 'emisor_rut':"
f" {referencia.rut_otro!r} == {emisor_rut!r},"
f" 'Referencia' number {referencia.numero_linea_ref}.",
f" 'Referencia' number {referencia.numero_linea_ref}."
)
if is_input_trusted_according_to_validation_context(info.context):
logger.warning('Validation failed but input is trusted: %s', message)
else:
raise ValueError(message)

return self

Expand Down
12 changes: 10 additions & 2 deletions src/cl_sii/dte/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData:
'ds:Signature', # "Firma Digital sobre Documento"
namespaces=xml_utils.XML_DSIG_NS_MAP,
)
assert signature_em is not None

if liquidacion_em is not None or exportaciones_em is not None:
raise NotImplementedError("XML element 'Documento' is the only one supported.")
Expand Down Expand Up @@ -191,6 +192,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData:
'sii-dte:Encabezado', # "Identificacion y Totales del Documento"
namespaces=DTE_XMLNS_MAP,
)
assert encabezado_em is not None
# note: excluded because currently it is not useful.
# ted_em = documento_em.find(
# 'sii-dte:TED', # "Timbre Electronico de DTE"
Expand All @@ -215,18 +217,22 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData:
'sii-dte:IdDoc', # "Identificacion del DTE"
namespaces=DTE_XMLNS_MAP,
)
assert id_doc_em is not None
emisor_em = encabezado_em.find(
'sii-dte:Emisor', # "Datos del Emisor"
namespaces=DTE_XMLNS_MAP,
)
assert emisor_em is not None
receptor_em = encabezado_em.find(
'sii-dte:Receptor', # "Datos del Receptor"
namespaces=DTE_XMLNS_MAP,
)
assert receptor_em is not None
totales_em = encabezado_em.find(
'sii-dte:Totales', # "Montos Totales del DTE"
namespaces=DTE_XMLNS_MAP,
)
assert totales_em is not None

# 'Documento.Encabezado.IdDoc'
# Excluded elements (optional according to the XML schema but the SII may require some of these
Expand Down Expand Up @@ -453,13 +459,15 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData:
'ds:KeyInfo', # "Informacion de Claves Publicas y Certificado"
namespaces=xml_utils.XML_DSIG_NS_MAP,
)
assert signature_key_info_em is not None
# signature_key_info_key_value_em = signature_key_info_em.find(
# 'ds:KeyValue',
# namespaces=xml_utils.XML_DSIG_NS_MAP)
signature_key_info_x509_data_em = signature_key_info_em.find(
'ds:X509Data', # "Informacion del Certificado Publico"
namespaces=xml_utils.XML_DSIG_NS_MAP,
)
assert signature_key_info_x509_data_em is not None
signature_key_info_x509_cert_em = signature_key_info_x509_data_em.find(
'ds:X509Certificate', # "Certificado Publico"
namespaces=xml_utils.XML_DSIG_NS_MAP,
Expand Down Expand Up @@ -523,7 +531,7 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteXmlData:
)


def _text_strip_or_none(xml_em: XmlElement) -> Optional[str]:
def _text_strip_or_none(xml_em: Optional[XmlElement]) -> Optional[str]:
# note: we need the pair of functions '_text_strip_or_none' and '_text_strip_or_raise'
# because, under certain circumstances, an XML tag:
# - with no content -> `xml_em.text` is None instead of ''
Expand All @@ -539,7 +547,7 @@ def _text_strip_or_none(xml_em: XmlElement) -> Optional[str]:
return stripped_text


def _text_strip_or_raise(xml_em: XmlElement) -> str:
def _text_strip_or_raise(xml_em: Optional[XmlElement]) -> str:
# note: we need the pair of functions '_text_strip_or_none' and '_text_strip_or_raise'
# because, under certain circumstances, an XML tag:
# - with no content -> `xml_em.text` is None instead of ''
Expand Down
6 changes: 5 additions & 1 deletion src/cl_sii/libs/xml_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ def verify_xml_signature(
trusted_x509_cert: Optional[Union[crypto_utils.X509Cert, crypto_utils._X509CertOpenSsl]] = None,
xml_verifier: Optional[signxml.verifier.XMLVerifier] = None,
xml_verifier_supports_multiple_signatures: bool = False,
) -> Tuple[bytes, XmlElementTree, XmlElementTree]:
) -> Tuple[
bytes,
Optional[Union[XmlElementTree, lxml.etree._Element]],
Union[XmlElementTree, lxml.etree._Element],
]:
"""
Verify the XML signature in ``xml_doc``.

Expand Down
Loading