├── docs └── .gitkeep ├── src ├── .keep ├── cl_sii │ ├── py.typed │ ├── base │ │ └── __init__.py │ ├── dte │ │ └── __init__.py │ ├── extras │ │ ├── __init__.py │ │ ├── drf_serializers.py │ │ ├── dj_url_converters.py │ │ ├── drf_fields.py │ │ ├── dj_form_fields.py │ │ └── dj_filters.py │ ├── contribuyente │ │ ├── __init__.py │ │ └── constants.py │ ├── __init__.py │ ├── libs │ │ ├── __init__.py │ │ ├── json_utils.py │ │ ├── csv_utils.py │ │ ├── encoding_utils.py │ │ ├── io_utils.py │ │ ├── dataclass_utils.py │ │ └── rows_processing.py │ ├── data │ │ ├── ref │ │ │ └── factura_electronica │ │ │ │ └── schemas-xml │ │ │ │ ├── AEC_v10.xsd │ │ │ │ ├── DTE_v10.xsd │ │ │ │ ├── Cesion_v10.xsd │ │ │ │ ├── LceCal_v10.xsd │ │ │ │ ├── .editorconfig │ │ │ │ ├── EnvioDTE_v10.xsd │ │ │ │ ├── LibroCV_v10.xsd │ │ │ │ ├── Recibos_v10.xsd │ │ │ │ ├── SiiTypes_v10.xsd │ │ │ │ ├── xmldsignature_v10.xsd │ │ │ │ ├── LceCoCertif_v10.xsd │ │ │ │ └── DTECedido_v10.xsd │ │ └── cte │ │ │ ├── f29_datos_obj_missing_key_fixes.json │ │ │ └── schemas-json │ │ │ └── f29_datos_obj.schema.json │ ├── cte │ │ ├── f29 │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── data_models.py │ ├── rtc │ │ ├── __init__.py │ │ ├── constants.py │ │ └── xml_utils.py │ ├── rcv │ │ └── __init__.py │ └── rut │ │ ├── crypto_utils.py │ │ └── constants.py ├── tests │ ├── __init__.py │ ├── test_dte.py │ ├── test_contribuyente.py │ ├── test_scripts_clean_dte_xml_file.py │ ├── test_scripts_canonicalize_xml_file.py │ ├── test_data │ │ ├── sii-crypto │ │ │ ├── prueba-sii-cert.der │ │ │ ├── TEST-DTE-13185095-K.der │ │ │ ├── TEST-DTE-WITH-ID-BUT-NO-RUT.der │ │ │ ├── DTE--76354771-K--33--170-cert.der │ │ │ ├── DTE--76399752-9--33--25568-cert.der │ │ │ ├── DTE--60910000-1--33--2336600-cert.der │ │ │ ├── DTE--96670340-7--61--110616-cert.der │ │ │ ├── AEC--76354771-K--33--170--SEQ-2-cert.der │ │ │ ├── AEC--76399752-9--33--25568--SEQ-1-cert.der │ │ │ ├── DTE--76354771-K--33--170-signature-value-base64.txt │ │ │ ├── DTE--96670340-7--61--110616-signature-value-base64.txt │ │ │ ├── AEC--76354771-K--33--170--SEQ-2-signature-value-base64.txt │ │ │ ├── DTE--76399752-9--33--25568-signature-value-base64.txt │ │ │ ├── DTE--60910000-1--33--2336600-signature-value-base64.txt │ │ │ ├── AEC--76399752-9--33--25568--SEQ-1-signature-value-base64.txt │ │ │ ├── prueba-sii-cert.pem │ │ │ ├── TEST-DTE-13185095-K.pem │ │ │ ├── TEST-DTE-WITH-ID-BUT-NO-RUT.pem │ │ │ ├── DTE--76399752-9--33--25568-cert.pem │ │ │ ├── AEC--76399752-9--33--25568--SEQ-1-cert.pem │ │ │ ├── DTE--96670340-7--61--110616-cert.pem │ │ │ ├── howto.md │ │ │ ├── DTE--60910000-1--33--2336600-cert.pem │ │ │ ├── AEC--76354771-K--33--170--SEQ-2-cert.pem │ │ │ └── DTE--76354771-K--33--170-cert.pem │ │ ├── xml │ │ │ ├── trivial.xml │ │ │ ├── attacks │ │ │ │ ├── external-entity-expansion-remote.xml │ │ │ │ ├── quadratic-blowup-entity-expansion.xml │ │ │ │ ├── billion-laughs-2.xml │ │ │ │ └── billion-laughs-1.xml │ │ │ └── trivial-doc.xml │ │ ├── crypto │ │ │ ├── wildcard-google-com-cert.der │ │ │ └── wildcard-google-com-cert.pem │ │ ├── sii-dte │ │ │ ├── DTE--76399752-9--33--25568.xml │ │ │ ├── DTE--60910000-1--33--2336600.xml │ │ │ ├── DTE--96670340-7--61--110616.xml │ │ │ ├── DTE--76399752-9--33--25568--cleaned.xml │ │ │ ├── DTE--96670340-7--61--110616--cleaned.xml │ │ │ ├── DTE--60910000-1--33--2336600--cleaned.xml │ │ │ ├── DTE--76354771-K--33--170--cleaned-signed_data.xml │ │ │ ├── DTE--76354771-K--33--170--cleaned-signed_xml.xml │ │ │ ├── DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml │ │ │ ├── DTE--76354771-K--33--170--cleaned-signature_xml.xml │ │ │ ├── DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml │ │ │ └── DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml │ │ ├── sii-rtc │ │ │ ├── AEC--76354771-K--33--170--SEQ-2.xml │ │ │ ├── AEC--76399752-9--33--25568--SEQ-1.xml │ │ │ └── AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n-signature_xml.xml │ │ ├── .editorconfig │ │ └── sii-rcv │ │ │ ├── RCV-compra-pendiente-rz_leading_trailing_whitespace.csv │ │ │ ├── RCV-compra-no_incluir-rz_leading_trailing_whitespace.csv │ │ │ ├── RCV-compra-reclamado-rz_leading_trailing_whitespace.csv │ │ │ ├── RCV-compra-registro-rz_leading_trailing_whitespace.csv │ │ │ ├── RCV-compra-reclamado.csv │ │ │ ├── RCV-venta-missing-required-fields.csv │ │ │ ├── RCV-venta-rz_leading_trailing_whitespace.csv │ │ │ └── RCV-venta-extra-empty-impuestos-rows.csv │ ├── test_libs_json_utils.py │ ├── test_libs_csv_utils.py │ ├── test_extras_drf_fields.py │ ├── test_libs_rows_processing.py │ ├── test_libs_mm_utils.py │ ├── cte_f29_factories.py │ ├── test_libs_encoding_utils.py │ ├── test_extras_drf_serializers.py │ ├── test_rtc_constants.py │ ├── utils.py │ ├── test_rut_constants.py │ ├── test_extras_dj_url_converters.py │ ├── test_extras_dj_filters.py │ ├── test_extras_dj_form_fields.py │ ├── test_libs_tz_utils.py │ └── test_cte_f29_data_models.py └── scripts │ ├── example.py │ ├── clean_dte_xml_file.py │ └── canonicalize_xml_file.py ├── setup.py ├── .bumpversion.cfg ├── .deepsource.toml ├── MANIFEST.in ├── commitlint.config.js ├── .flake8 ├── .black.cfg.toml ├── .coveragerc.test.ini ├── CODEOWNERS ├── .editorconfig-checker.json ├── requirements-dev.in ├── tox.ini ├── .github ├── workflows │ ├── git-commit-lint.yaml │ ├── dependency-review.yaml │ ├── super-linter.yaml │ ├── deploy.yaml │ ├── ci-cd.yaml │ ├── release.yaml │ ├── task-release-and-deploy.yaml │ └── ci.yaml └── dependabot.yml ├── .markdownlint.yaml ├── .isort.cfg ├── requirements.in ├── .editorconfig ├── LICENSE ├── mypy.ini ├── .git-blame-ignore-revs ├── requirements.txt ├── README.md └── pyproject.toml /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cl_sii/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cl_sii/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cl_sii/dte/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cl_sii/extras/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cl_sii/contribuyente/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tests/test_dte.py: -------------------------------------------------------------------------------- 1 | from cl_sii.dte import constants # noqa: F401 2 | -------------------------------------------------------------------------------- /src/tests/test_contribuyente.py: -------------------------------------------------------------------------------- 1 | from cl_sii.contribuyente import constants # noqa: F401 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup() 7 | -------------------------------------------------------------------------------- /src/tests/test_scripts_clean_dte_xml_file.py: -------------------------------------------------------------------------------- 1 | # TODO: implement tests for script 'clean_dte_xml_file.py' 2 | -------------------------------------------------------------------------------- /src/tests/test_scripts_canonicalize_xml_file.py: -------------------------------------------------------------------------------- 1 | # TODO: Implement tests for script 'canonicalize_xml_file.py'. 2 | -------------------------------------------------------------------------------- /src/cl_sii/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | cl-sii Python lib 3 | ================= 4 | 5 | """ 6 | 7 | __version__ = '0.67.0' 8 | -------------------------------------------------------------------------------- /src/cl_sii/libs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package that contains code useful for :mod:`cl_sii` but not particular to it. 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/prueba-sii-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/prueba-sii-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/xml/trivial.xml: -------------------------------------------------------------------------------- 1 | 2 | text 3 | texttail 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/tests/test_data/crypto/wildcard-google-com-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/crypto/wildcard-google-com-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--60910000-1--33--2336600.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-dte/DTE--60910000-1--33--2336600.xml -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--96670340-7--61--110616.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-dte/DTE--96670340-7--61--110616.xml -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/DTE_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/DTE_v10.xsd -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/LceCal_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/LceCal_v10.xsd -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.xsd] 2 | end_of_line = unset 3 | indent_style = unset 4 | trim_trailing_whitespace = unset 5 | insert_final_newline = unset 6 | -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/EnvioDTE_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/EnvioDTE_v10.xsd -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/LibroCV_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/LibroCV_v10.xsd -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--60910000-1--33--2336600-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/DTE--60910000-1--33--2336600-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--96670340-7--61--110616-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/DTE--96670340-7--61--110616-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--96670340-7--61--110616--cleaned.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-dte/DTE--96670340-7--61--110616--cleaned.xml -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/AEC--76354771-K--33--170--SEQ-2-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/AEC--76354771-K--33--170--SEQ-2-cert.der -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--60910000-1--33--2336600--cleaned.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-dte/DTE--60910000-1--33--2336600--cleaned.xml -------------------------------------------------------------------------------- /src/cl_sii/cte/f29/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Declaraciones de IVA (Formulario 29) – Carpeta Tributaria Electrónica (CTE) 3 | =========================================================================== 4 | """ 5 | -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/xmldsignature_v10.xsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/cl_sii/data/ref/factura_electronica/schemas-xml/xmldsignature_v10.xsd -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/AEC--76399752-9--33--25568--SEQ-1-cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cordada/lib-cl-sii-python/HEAD/src/tests/test_data/sii-crypto/AEC--76399752-9--33--25568--SEQ-1-cert.der -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.67.0 3 | commit = True 4 | tag = False 5 | message = chore: Bump version from {current_version} to {new_version} 6 | 7 | [bumpversion:file:src/cl_sii/__init__.py] 8 | 9 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [ 4 | "src/cl_sii/**" 5 | ] 6 | 7 | [[analyzers]] 8 | name = "python" 9 | enabled = true 10 | 11 | [analyzers.meta] 12 | runtime_version = "3.x.x" 13 | max_line_length = 100 14 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt: -------------------------------------------------------------------------------- 1 | fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnkddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKiwqSxDcYjTT6vXsLPrZk= 2 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--96670340-7--61--110616-signature-value-base64.txt: -------------------------------------------------------------------------------- 1 | tklxn/gxaX16GGNmk4chkBoAJHUOaWm1XDLGCCRPpZQ8ireiXel+c1+Nxen4x+tzAAX2tJ1CiGuKeuDy1Kd5c+5ngdtMqFrtBiOIXI2xLBUHVcVN87UzLBbH5wGVbLOaT1L7QX+4A5wm3qKWGyvDgCHRbemWqJedpswZ3ZiDBbE= 2 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/AEC--76354771-K--33--170--SEQ-2-signature-value-base64.txt: -------------------------------------------------------------------------------- 1 | dHnmsO61gKLtSMbHtOooDjBbcjNMsQjkE/sAehhTlSiXw830v/We+nXYhpFpCsA0AaqBlMC6RQN0bOvxsGNJI477apkGOGgj87MOs02hiI5pmmtVsn+ohMv+Vc/ISEN3qVIfyWF02EFXVbvnifhq1dSWDp78RKH/9Tqaq4ww+N0= 2 | -------------------------------------------------------------------------------- /src/tests/test_data/xml/attacks/external-entity-expansion-remote.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | ]> 5 | 6 | -------------------------------------------------------------------------------- /src/tests/test_data/xml/attacks/quadratic-blowup-entity-expansion.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | ]> 5 | &a;&a;&a;... repeat 6 | -------------------------------------------------------------------------------- /src/tests/test_libs_json_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cl_sii.libs.json_utils import read_json_schema # noqa: F401 4 | from .utils import read_test_file_json_dict # noqa: F401 5 | 6 | 7 | class FunctionReadJsonSchemaTest(unittest.TestCase): 8 | # TODO: implement 9 | 10 | pass 11 | -------------------------------------------------------------------------------- /src/cl_sii/rtc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SII RTC/RPETC 3 | ============= 4 | 5 | Concepts and acronyms used interchangeably: 6 | 7 | * "Registro Transferencia de Crédito" (RTC) 8 | * "Registro Público Electrónico de Transferencia de Crédito" (RPETC) 9 | * "Registro Electrónico de Cesión de Créditos" 10 | """ 11 | -------------------------------------------------------------------------------- /src/tests/test_data/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.der] 2 | end_of_line = unset 3 | insert_final_newline = unset 4 | 5 | [*.pem] 6 | end_of_line = unset 7 | insert_final_newline = unset 8 | 9 | [*.xml] 10 | end_of_line = unset 11 | indent_style = unset 12 | trim_trailing_whitespace = unset 13 | insert_final_newline = unset 14 | -------------------------------------------------------------------------------- /src/tests/test_libs_csv_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cl_sii.libs.csv_utils import create_csv_dict_reader # noqa: F401 4 | 5 | 6 | class FunctionsTest(unittest.TestCase): 7 | def test_create_csv_dict_reader(self) -> None: 8 | # TODO: implement for 'create_csv_dict_reader'. 9 | pass 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.md 2 | include LICENSE 3 | include README.md 4 | include src/cl_sii/data/cte/*.json 5 | recursive-include src/cl_sii *py 6 | recursive-include src/cl_sii/data/cte/schemas-json *.schema.json 7 | recursive-include src/cl_sii/data/ref/factura_electronica/schemas-xml *.xsd 8 | include src/cl_sii/py.typed 9 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Commitlint Configuration 3 | 4 | Commitlint is a Git commit message linter. 5 | 6 | - Web site: https://commitlint.js.org/ 7 | - Documentation: https://github.com/conventional-changelog/commitlint#readme 8 | */ 9 | 10 | module.exports = { extends: ["@cordada/commitlint-config-cordada"] }; 11 | -------------------------------------------------------------------------------- /src/tests/test_data/xml/attacks/billion-laughs-2.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | ]> 8 | &d; 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # W503 line break before binary operator 4 | W503 5 | 6 | exclude = 7 | *.egg-info/, 8 | .git/, 9 | .mypy_cache/, 10 | .pyenvs/, 11 | __pycache__/, 12 | build/, 13 | dist/, 14 | docs/ 15 | 16 | max-line-length = 100 17 | 18 | doctests = True 19 | show-source = True 20 | statistics = True 21 | -------------------------------------------------------------------------------- /.black.cfg.toml: -------------------------------------------------------------------------------- 1 | # Black Configuration 2 | # 3 | # Black is a Python source code formatter. 4 | # 5 | # - Web site: https://github.com/psf/black/ 6 | # - Documentation: https://black.readthedocs.io/ 7 | 8 | [tool.black] 9 | include = '\.pyi?$' 10 | line-length = 100 11 | skip-string-normalization = true 12 | target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] 13 | -------------------------------------------------------------------------------- /.coveragerc.test.ini: -------------------------------------------------------------------------------- 1 | [run] 2 | source = src/ 3 | omit = 4 | src/scripts/* 5 | src/tests/* 6 | branch = True 7 | 8 | [report] 9 | exclude_lines = 10 | pragma: no cover 11 | if __name__ == .__main__. 12 | show_missing = True 13 | 14 | [xml] 15 | output = test-reports/coverage/xml/coverage.xml 16 | 17 | [html] 18 | directory = test-reports/coverage/html 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code Owners 2 | # 3 | # Use this file to define individuals or teams that are responsible for code in a repository. 4 | # 5 | # Documentation: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 6 | 7 | # Default 8 | * @cordada/developers 9 | 10 | # Dependencies 11 | /requirements.* 12 | /requirements-*.* 13 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt: -------------------------------------------------------------------------------- 1 | wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFDMrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg== 2 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--60910000-1--33--2336600-signature-value-base64.txt: -------------------------------------------------------------------------------- 1 | JnuSNM2A6cKJcE45HmP5iyYs7MgIpluKam+t7YRfVVMrtTeJmTfFftcj8jVDqKEGtNAqFQvjaVubmxqc/GxiEIZv27WRfBwD71bL9F2Cgjxw6+utDgY51be6vHkMPP3MCUX/uB+Z1Zn9jM4U/RGznE817aY4a2PUFqd6b1UuE0HKo/RwQAcGsxZXGolZ3nUF443QiL6riR+e8MaSSJLJbGcykJlHh44MNCYRYoYbwuAp6mAg5h82z8Eazi8F+C0SZvQKITCgd1do+GZgNGUJB1QaK/I5rE7WDfhr5oOwqk1FeQMIL/QZqHOReIyoHr7WVXIpNQaO/HKlV46dSuWzZA== 2 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/AEC--76399752-9--33--25568--SEQ-1-signature-value-base64.txt: -------------------------------------------------------------------------------- 1 | hBUBX/XDhmNokXXfZ7R3drK78N5SX8xLn6sYyAaBTut4wILA4kHB9BW45oV0wS/A 2 | 53l7EX5yg42KHRXQ+vVzc5R+zYpGvgAPnv8eM2lCQKmyEdhR0YoQ1YnRL/7vchJ2 3 | 8TnrTxSMMePj589rOAUD8IeTr1vKyfdih+r6maTA6C+O2dzVf3zl/GtTstoZdX2B 4 | ZEf6/yzX9T7kFQ27zZ3WKGLFFjQKaQa2Nh/dIPEcfci1KgCZhozGPw9++xPG3P9I 5 | ewG3h95UvHjL1jOag3grvrEG+yCYlUpMq4vnUTuGfbwcW7nYq+HSU0IKDPccmzlh 6 | PCUn28yVEm+JlH0/P8QL3w== 7 | -------------------------------------------------------------------------------- /src/tests/test_extras_drf_fields.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import rest_framework # noqa: F401 4 | 5 | 6 | # TODO: create a test setup that at least makes it possible to run the following imports 7 | # (underlying `import rest_framework.fields` raises Django's 'ImproperlyConfigured'): 8 | # from cl_sii.extras.drf_fields import Rut, RutField 9 | 10 | 11 | class RutFieldTest(unittest.TestCase): 12 | # TODO: implement! 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /src/cl_sii/rcv/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SII RCV ("Registro de Compras y Ventas") 3 | ======================================== 4 | 5 | The RCV ("Registro de Compras y Ventas") is composed of 2 "registros": 6 | RC ("Registro de Compras") and RV ("Registro de Ventas"). 7 | 8 | .. seealso:: 9 | See SII / FAQ / 10 | `¿Qué es el Registro de Compras y Ventas (RCV)? `_ # noqa: E501 11 | 12 | """ 13 | -------------------------------------------------------------------------------- /.editorconfig-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "Verbose": false, 3 | "Debug": false, 4 | "IgnoreDefaults": false, 5 | "SpacesAfterTabs": false, 6 | "NoColor": false, 7 | "Exclude": [], 8 | "AllowedContentTypes": [], 9 | "PassedFiles": [], 10 | "Disable": { 11 | "EndOfLine": false, 12 | "Indentation": false, 13 | "InsertFinalNewline": false, 14 | "TrimTrailingWhitespace": false, 15 | "IndentSize": true, 16 | "MaxLineLength": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /requirements-dev.in: -------------------------------------------------------------------------------- 1 | # Python Dependency Manifest 2 | # 3 | # Deployment environment: development, testing, release 4 | 5 | -c requirements.txt 6 | 7 | black==25.11.0 8 | build==1.3.0 9 | bumpversion==0.5.3 10 | coverage==7.10.7 11 | flake8==7.3.0 12 | isort==6.1.0 13 | mypy==1.19.0 14 | pip-tools==7.5.2 15 | tox==4.27.0 16 | twine==6.2.0 17 | types-jsonschema==4.25.0.20250809 18 | types-lxml==2025.11.25 19 | types-pytz==2025.2.0.20250809 20 | types-setuptools==80.9.0.20250822 21 | -------------------------------------------------------------------------------- /src/cl_sii/data/cte/f29_datos_obj_missing_key_fixes.json: -------------------------------------------------------------------------------- 1 | { 2 | "glosa": { 3 | "049": "(Desconocido)", 4 | "098": "(Desconocido)", 5 | "100": "(Desconocido)", 6 | "103": "(Desconocido)", 7 | "156": "BASE IMPTO.A74,T.V.", 8 | "157": "RET..ART74,T.VAR", 9 | "8020": "(Desconocido)" 10 | }, 11 | "tipos": { 12 | "049": "M", 13 | "098": "M", 14 | "100": "M", 15 | "103": "M", 16 | "156": "M", 17 | "157": "M", 18 | "8020": "M" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py39, 4 | py310, 5 | py311, 6 | py312, 7 | py313, 8 | 9 | [testenv] 10 | setenv = 11 | PYTHONPATH = {toxinidir}:{toxinidir}/cl_sii 12 | commands = coverage run --rcfile=.coveragerc.test.ini -m unittest discover -v -c -b -s src -t src 13 | deps = 14 | -r{toxinidir}/requirements.txt 15 | -r{toxinidir}/requirements-dev.txt 16 | basepython = 17 | py39: python3.9 18 | py310: python3.10 19 | py311: python3.11 20 | py312: python3.12 21 | py313: python3.13 22 | -------------------------------------------------------------------------------- /.github/workflows/git-commit-lint.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for Git Commit Linter 2 | 3 | name: Git Commit Linter 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - ready_for_review 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | git-commit-lint: 22 | name: Git Commit Linter 23 | uses: cordada/github-actions-utils/.github/workflows/git-commit-lint.yaml@master 24 | -------------------------------------------------------------------------------- /src/tests/test_libs_rows_processing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cl_sii.libs.rows_processing import ( # noqa: F401 4 | csv_rows_mm_deserialization_iterator, 5 | rows_mm_deserialization_iterator, 6 | ) 7 | 8 | 9 | class FunctionsTest(unittest.TestCase): 10 | def test_csv_rows_mm_deserialization_iterator(self) -> None: 11 | # TODO: implement for 'csv_rows_mm_deserialization_iterator'. 12 | pass 13 | 14 | def test_rows_mm_deserialization_iterator(self) -> None: 15 | # TODO: implement for 'rows_mm_deserialization_iterator'. 16 | pass 17 | -------------------------------------------------------------------------------- /src/tests/test_libs_mm_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cl_sii.libs.mm_utils import ( # noqa: F401 4 | CustomMarshmallowDateField, 5 | validate_no_unexpected_input_fields, 6 | ) 7 | 8 | 9 | class CustomMarshmallowDateFieldTest(unittest.TestCase): 10 | def test_x(self) -> None: 11 | # TODO: implement for 'CustomMarshmallowDateField'. 12 | pass 13 | 14 | 15 | class FunctionsTest(unittest.TestCase): 16 | def test_validate_no_unexpected_input_fields(self) -> None: 17 | # TODO: implement for 'validate_no_unexpected_input_fields'. 18 | pass 19 | -------------------------------------------------------------------------------- /src/cl_sii/contribuyente/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contribuyente-related constants. 3 | 4 | Source: XML types 'RznSocLargaType' and 'RznSocCortaType' in official schema 5 | 'SiiTypes_v10.xsd'. 6 | https://github.com/fyntex/lib-cl-sii-python/blob/f57a326/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd#L635-L651 7 | 8 | """ 9 | 10 | # TODO: RAZON_SOCIAL_LONG_REGEX = re.compile(r'^...$') 11 | 12 | RAZON_SOCIAL_LONG_MAX_LENGTH = 100 13 | """"Razón Social" max length ("long version").""" 14 | 15 | RAZON_SOCIAL_SHORT_MAX_LENGTH = 40 16 | """"Razón Social" max length ("short version").""" 17 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Markdownlint Configuration 2 | # 3 | # Markdownlint is a style checker and lint tool for Markdown/CommonMark files. 4 | # 5 | # - Web site: https://github.com/DavidAnson/markdownlint/ 6 | # - Documentation: https://github.com/DavidAnson/markdownlint#readme 7 | 8 | default: true 9 | 10 | heading-style: 11 | style: atx 12 | 13 | line-length: 14 | line_length: 100 15 | code_block_line_length: 200 16 | tables: false 17 | 18 | no-duplicate-heading: 19 | allow_different_nesting: true 20 | 21 | no-emphasis-as-heading: false 22 | 23 | no-multiple-blanks: 24 | maximum: 2 25 | 26 | ul-indent: 27 | indent: 2 28 | -------------------------------------------------------------------------------- /src/tests/cte_f29_factories.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | from typing import Any 5 | 6 | from cl_sii.cte.f29 import data_models 7 | from cl_sii.rcv.data_models import PeriodoTributario 8 | from cl_sii.rut import Rut 9 | 10 | 11 | def create_CteForm29(**kwargs: Any) -> data_models.CteForm29: 12 | defaults = dict( 13 | contribuyente_rut=Rut.random(), 14 | periodo_tributario=PeriodoTributario(year=2018, month=random.randint(1, 12)), 15 | folio=random.randint(1, 9999999999), 16 | ) 17 | defaults.update(kwargs) 18 | 19 | obj = data_models.CteForm29(**defaults) 20 | return obj 21 | -------------------------------------------------------------------------------- /src/tests/test_libs_encoding_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from cl_sii.libs.encoding_utils import ( # noqa: F401 4 | clean_base64, 5 | decode_base64_strict, 6 | validate_base64, 7 | ) 8 | 9 | 10 | class FunctionsTest(unittest.TestCase): 11 | def test_clean_base64(self) -> None: 12 | # TODO: implement for function 'clean_base64'. 13 | pass 14 | 15 | def test_decode_base64_strict(self) -> None: 16 | # TODO: implement for function 'decode_base64_strict'. 17 | pass 18 | 19 | def test_validate_base64(self) -> None: 20 | # TODO: implement for function 'validate_base64'. 21 | pass 22 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-compra-pendiente-rz_leading_trailing_whitespace.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Compra;RUT Proveedor;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Monto Exento;Monto Neto;Monto IVA Recuperable;Monto Iva No Recuperable;Codigo IVA No Rec.;Monto Total;Monto Neto Activo Fijo;IVA Activo Fijo;IVA uso Comun;Impto. Sin Derecho a Credito;IVA No Retenido;NCE o NDE sobre Fact. de Compra;Codigo Otro Impuesto;Valor Otro Impuesto;Tasa Otro Impuesto 2 | 1;33;Del Giro;12345678-5;Fake Company S.A. ;9800042;28/06/2019;01/07/2019 13:21:32;0;41838;7949;;;49787;;;;;0;0;;;; 3 | 7;33;Del Giro;12345678-5; Fake Company S.A.;380007;01/07/2019;02/07/2019 17:34:28;0;54411;10338;;;64749;;;;;0;0;;;; 4 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | # Isort Configuration 2 | # 3 | # Isort is a Python import sorter. 4 | # 5 | # - Web site: https://github.com/PyCQA/isort/ 6 | # - Configuration options: https://pycqa.github.io/isort/docs/configuration/options/ 7 | 8 | [settings] 9 | combine_as_imports=false 10 | ensure_newline_before_comments=true 11 | extra_standard_library= 12 | force_grid_wrap=0 13 | force_single_line=false 14 | include_trailing_comma=true 15 | known_first_party= 16 | known_local_folder= 17 | known_third_party= 18 | line_length=100 19 | lines_after_imports=2 20 | multi_line_output=3 21 | no_lines_before=LOCALFOLDER 22 | sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 23 | skip_gitignore=true 24 | use_parentheses=true 25 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # Python Dependency Manifest 2 | # 3 | # Deployment environment: base 4 | 5 | # Note: To install a package from a Git VCS repository, see the following example: 6 | # git+https://github.com/example/example.git@example-vcs-ref#egg=example-pkg[foo,bar]==1.42.3 7 | 8 | backports-zoneinfo==0.2.1 ; python_version < "3.9" # Used by `djangorestframework`. 9 | cryptography==45.0.7 10 | defusedxml==0.7.1 11 | django-filter>=24.2 12 | Django>=4.2 13 | djangorestframework>=3.10.3,<3.17 14 | importlib-metadata==8.7.0 15 | jsonschema==4.25.1 16 | lxml==6.0.2 17 | marshmallow==4.0.1 18 | pydantic==2.11.7 19 | pyOpenSSL==25.1.0 20 | pytz==2025.2 21 | setuptools==80.9.0 22 | signxml==4.2.0 23 | typing-extensions==4.15.0 24 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-compra-no_incluir-rz_leading_trailing_whitespace.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Compra;RUT Proveedor;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse;Monto Exento;Monto Neto;Monto IVA Recuperable;Monto Iva No Recuperable;Codigo IVA No Rec.;Monto Total;Monto Neto Activo Fijo;IVA Activo Fijo;IVA uso Comun;Impto. Sin Derecho a Credito;IVA No Retenido;NCE o NDE sobre Fact. de Compra;Codigo Otro Impuesto;Valor Otro Impuesto;Tasa Otro Impuesto 2 | 1;33;No Corresp. Incluir;12345678-5;Fake Company S.A. ;19000035;13/12/2019;14/12/2019 15:56:27;;0;87699;;16663;9;104362;;;;;0;0;;;; 3 | 2;33;No Corresp. Incluir;12345678-5; Fake Company S.A.;19000036;13/12/2019;14/12/2019 15:56:27;;0;155473;;29540;9;185013;;;;;0;0;;;; 4 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yaml: -------------------------------------------------------------------------------- 1 | name: Dependency Review 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | - ready_for_review 10 | 11 | permissions: 12 | contents: read 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | dependency-review: 20 | name: Dependency Review 21 | runs-on: ubuntu-22.04 22 | 23 | steps: 24 | - name: Check Out VCS Repository 25 | uses: actions/checkout@v6.0.0 26 | 27 | - name: Dependency Review 28 | uses: actions/dependency-review-action@v4.8.2 29 | with: 30 | fail-on-severity: critical 31 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-compra-reclamado-rz_leading_trailing_whitespace.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Compra;RUT Proveedor;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA Recuperable;Monto Iva No Recuperable;Codigo IVA No Rec.;Monto Total;Monto Neto Activo Fijo;IVA Activo Fijo;IVA uso Comun;Impto. Sin Derecho a Credito;IVA No Retenido;NCE o NDE sobre Fact. de Compra;Codigo Otro Impuesto;Valor Otro Impuesto;Tasa Otro Impuesto 2 | 1;33;Del Giro;12345678-5;Fake Company S.A. ;1000055;05/06/2019;05/06/2019 21:58:49;12/06/2019 09:47:23;0;970894;184470;;;1155364;;;;;0;0;;;; 3 | 2;61;Del Giro;12345678-5; Fake Company S.A.;70013;24/06/2019;24/06/2019 15:24:41;null;0;1652840;314040;;;1966880;;;;;0;0;;;; 4 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-compra-registro-rz_leading_trailing_whitespace.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Compra;RUT Proveedor;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse;Monto Exento;Monto Neto;Monto IVA Recuperable;Monto Iva No Recuperable;Codigo IVA No Rec.;Monto Total;Monto Neto Activo Fijo;IVA Activo Fijo;IVA uso Comun;Impto. Sin Derecho a Credito;IVA No Retenido;Tabacos Puros;Tabacos Cigarrillos;Tabacos Elaborados;NCE o NDE sobre Fact. de Compra;Codigo Otro Impuesto;Valor Otro Impuesto;Tasa Otro Impuesto 2 | 1;33;Del Giro;12345678-5;Fake Company S.A. ;23084;21/06/2019;24/06/2019 09:55:53;;0;240169;45632;;;285801;;;;;0;;;;0;;;; 3 | 7;33;Del Giro;12345678-5; Fake Company S.A.;31000097;02/07/2019;03/07/2019 00:15:30;;0;27356;5198;;;32554;;;;;0;;;;0;;;; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | max_line_length = 100 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{ini,py,rst}] 15 | indent_style = space 16 | indent_size = 4 17 | 18 | [*.py] 19 | multi_line_output = 3 20 | 21 | [*.{css,html,js,json,scss,xml,yml,yaml}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | # minified JavaScript files should not be modified 26 | [**.min.js] 27 | indent_style = unset 28 | insert_final_newline = unset 29 | 30 | [*.md] 31 | indent_style = space 32 | indent_size = 2 33 | 34 | [*.{diff,patch}] 35 | trim_trailing_whitespace = false 36 | 37 | [*.sh] 38 | indent_style = tab 39 | 40 | [Makefile] 41 | indent_style = tab 42 | -------------------------------------------------------------------------------- /src/tests/test_data/xml/trivial-doc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1 6 | 2008 7 | 141100 8 | 9 | 10 | 11 | 12 | 4 13 | 2011 14 | 59900 15 | 16 | 17 | 18 | 68 19 | 2011 20 | 13600 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/tests/test_extras_drf_serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | 5 | import django.db.models 6 | import rest_framework.serializers 7 | 8 | import cl_sii.extras.drf_serializers 9 | 10 | 11 | class ModelSerializerFieldMappingTestCase(unittest.TestCase): 12 | """ 13 | Tests for :attr:`model_serializer_field_mapping`. 14 | """ 15 | 16 | def test_types(self) -> None: 17 | serializer_field_mapping = { 18 | **rest_framework.serializers.ModelSerializer.serializer_field_mapping, 19 | **cl_sii.extras.drf_serializers.model_serializer_field_mapping, 20 | } 21 | 22 | for k, v in serializer_field_mapping.items(): 23 | self.assertTrue(issubclass(k, django.db.models.Field)) 24 | self.assertTrue(issubclass(v, rest_framework.serializers.Field)) 25 | -------------------------------------------------------------------------------- /src/tests/test_rtc_constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | from typing import ClassVar 5 | 6 | from cl_sii.dte.constants import TipoDte 7 | from cl_sii.rtc import constants 8 | 9 | 10 | class TipoDteCediblesTest(unittest.TestCase): 11 | """ 12 | Tests for `TIPO_DTE_CEDIBLES`. 13 | """ 14 | 15 | TIPO_DTE_CEDIBLES: ClassVar[frozenset[TipoDte]] 16 | 17 | @classmethod 18 | def setUpClass(cls) -> None: 19 | super().setUpClass() 20 | 21 | cls.TIPO_DTE_CEDIBLES = constants.TIPO_DTE_CEDIBLES 22 | 23 | def test_all_are_factura(self) -> None: 24 | for element in self.TIPO_DTE_CEDIBLES: 25 | with self.subTest(name=element.name): 26 | self.assertTrue(element.is_factura) 27 | 28 | # TODO: implement test that check that the values correspond to those defined in 29 | # XML type 'SiiDte:DTEFacturasType' in official schema 'SiiTypes_v10.xsd'. 30 | -------------------------------------------------------------------------------- /src/tests/test_data/xml/attacks/billion-laughs-1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ]> 16 | &lol9; 17 | -------------------------------------------------------------------------------- /.github/workflows/super-linter.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for Super-Linter 2 | # 3 | # Super-Linter is a simple combination of various linters, written in bash, to help validate your 4 | # source code. 5 | # 6 | # - Web site: https://github.com/github/super-linter/ 7 | # - Documentation: https://github.com/github/super-linter#readme 8 | 9 | name: Super-Linter 10 | 11 | on: 12 | push: 13 | branches: 14 | - develop 15 | - master 16 | pull_request: 17 | types: 18 | - opened 19 | - reopened 20 | - synchronize 21 | 22 | permissions: 23 | contents: read 24 | statuses: write 25 | checks: write 26 | 27 | concurrency: 28 | group: ${{ github.workflow }}-${{ github.ref }} 29 | cancel-in-progress: true 30 | 31 | jobs: 32 | super-linter: 33 | name: Super-Linter 34 | uses: cordada/github-actions-utils/.github/workflows/super-linter.yaml@master 35 | with: 36 | default_git_branch: develop 37 | validate_all_codebase: false 38 | 39 | validate_editorconfig: true 40 | validate_markdown: true 41 | -------------------------------------------------------------------------------- /src/cl_sii/cte/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Carpeta Tributaria Electrónica (CTE) 3 | ==================================== 4 | 5 | Official FAQ: 6 | http://www.sii.cl/preguntas_frecuentes/carp_trib_electronica/preg_carp_trib_electronica.htm 7 | 8 | 9 | ¿La Carpeta Tributaria Electrónica [..] puede ser utilizada como certificado? 10 | 11 | > Sí, la Carpeta Tributaria Electrónica de Internet puede ser utilizada como 12 | > certificado. 13 | > El SII, por su parte, garantiza que la información contenida en la carpeta 14 | > sea vigente y válida para la fecha y hora en que sea generada. 15 | 16 | Source: http://www.sii.cl/preguntas_frecuentes/carp_trib_electronica/001_045_5787.htm 17 | 18 | 19 | ¿Qué es y para qué sirve la Carpeta Tributaria Electrónica [..]? 20 | 21 | > La Carpeta Tributaria Electrónica permitirá entregar la información 22 | > tributaria de un contribuyente a un tercero autorizado, con el objeto de 23 | > solicitar créditos bancarios, acreditar rentas, entre otras. 24 | 25 | Source: http://www.sii.cl/preguntas_frecuentes/carp_trib_electronica/001_045_5776.htm 26 | """ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Fyntex TI SpA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-compra-reclamado.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Compra;RUT Proveedor;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA Recuperable;Monto Iva No Recuperable;Codigo IVA No Rec.;Monto Total;Monto Neto Activo Fijo;IVA Activo Fijo;IVA uso Comun;Impto. Sin Derecho a Credito;IVA No Retenido;NCE o NDE sobre Fact. de Compra;Codigo Otro Impuesto;Valor Otro Impuesto;Tasa Otro Impuesto 2 | 1;33;Del Giro;12345678-5;Fake Company S.A. ;1000055;05/06/2019;05/06/2019 21:58:49;12/06/2019 09:47:23;0;970894;184470;;;1155364;;;;;0;0;23;12000;2.967999999; 3 | 2;61;Del Giro;12345678-5; Fake Company S.A.;70013;24/06/2019;24/06/2019 15:24:41;null;0;1652840;314040;;;1966880;;;;;0;0;;;; 4 | 3;33;Del Giro;76354771-K;Fake Company S.A. ;789456;05/06/2019;05/06/2019 21:58:49;;0;970894;184470;;;1155364;;;;;0;0;;;; 5 | 4;33;Del Giro;INVALID-RUT;Fake Company S.A.;notanumber;invalid-date;invalid-datetime;invalid-datetime;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber;notanumber 6 | 5;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 7 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-venta-missing-required-fields.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Venta;Rut cliente;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse Recibo;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA;Monto total;IVA Retenido Total;IVA Retenido Parcial;IVA no retenido;IVA propio;IVA Terceros;RUT Emisor Liquid. Factura;Neto Comision Liquid. Factura;Exento Comision Liquid. Factura;IVA Comision Liquid. Factura;IVA fuera de plazo;Tipo Docto. Referencia;Folio Docto. Referencia;Num. Ident. Receptor Extranjero;Nacionalidad Receptor Extranjero;Credito empresa constructora;Impto. Zona Franca (Ley 18211);Garantia Dep. Envases;Indicador Venta sin Costo;Indicador Servicio Periodico;Monto No facturable;Total Monto Periodo;Venta Pasajes Transporte Nacional;Venta Pasajes Transporte Internacional;Numero Interno;Codigo Sucursal;NCE o NDE sobre Fact. de Compra;Codigo Otro Imp.;Valor Otro Imp.;Tasa Otro Imp. 2 | 1;"";Del Giro;12345678-5;Fake Company S.A. ;506;04/06/2019;"";;;0;1750181;332534;2082715;0;0;0;0;0;-;0;0;0;0;;;;;0;;0;2;0;0;0;;;;0;;;;; 3 | 23;33;Del Giro;12345678-5; Fake Company S.A.;508;28/06/2019;01/07/2019 13:49:42;;;0;2209597;419823;2629420;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;0;;;;; 4 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-venta-rz_leading_trailing_whitespace.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Venta;Rut cliente;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse Recibo;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA;Monto total;IVA Retenido Total;IVA Retenido Parcial;IVA no retenido;IVA propio;IVA Terceros;RUT Emisor Liquid. Factura;Neto Comision Liquid. Factura;Exento Comision Liquid. Factura;IVA Comision Liquid. Factura;IVA fuera de plazo;Tipo Docto. Referencia;Folio Docto. Referencia;Num. Ident. Receptor Extranjero;Nacionalidad Receptor Extranjero;Credito empresa constructora;Impto. Zona Franca (Ley 18211);Garantia Dep. Envases;Indicador Venta sin Costo;Indicador Servicio Periodico;Monto No facturable;Total Monto Periodo;Venta Pasajes Transporte Nacional;Venta Pasajes Transporte Internacional;Numero Interno;Codigo Sucursal;NCE o NDE sobre Fact. de Compra;Codigo Otro Imp.;Valor Otro Imp.;Tasa Otro Imp. 2 | 1;33;Del Giro;12345678-5;Fake Company S.A. ;506;04/06/2019;18/06/2019 17:01:06;;;0;1750181;332534;2082715;0;0;0;0;0;-;0;0;0;0;;;;;0;;0;2;0;0;0;;;;0;;;;; 3 | 23;33;Del Giro;12345678-5; Fake Company S.A.;508;28/06/2019;01/07/2019 13:49:42;;;0;2209597;419823;2629420;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;0;;;;; 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # GitHub Dependabot Configuration 2 | # 3 | # Dependabot can maintain your repository’s dependencies automatically. 4 | # 5 | # Documentation: 6 | # - https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference 7 | # - https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 8 | # - https://github.com/dependabot/dependabot-core/#readme 9 | 10 | version: 2 11 | 12 | updates: 13 | - package-ecosystem: pip 14 | directory: / 15 | ignore: 16 | - dependency-name: "bumpversion" 17 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 18 | groups: 19 | python-development: 20 | dependency-type: development 21 | exclude-patterns: 22 | - "bumpversion" 23 | - "pip" 24 | - "pip-tools" 25 | - "setuptools" 26 | - "types-*" 27 | schedule: 28 | interval: monthly 29 | commit-message: 30 | prefix: "chore(deps):" 31 | labels: 32 | - dependencies 33 | open-pull-requests-limit: 5 34 | 35 | - package-ecosystem: github-actions 36 | directory: / 37 | groups: 38 | github-actions-production: 39 | dependency-type: production 40 | schedule: 41 | interval: monthly 42 | commit-message: 43 | prefix: "chore(deps):" 44 | labels: 45 | - dependencies 46 | open-pull-requests-limit: 5 47 | -------------------------------------------------------------------------------- /src/cl_sii/extras/drf_serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | try: 5 | import rest_framework 6 | except ImportError as exc: # pragma: no cover 7 | raise ImportError("Package 'djangorestframework' is required to use this module.") from exc 8 | try: 9 | import django 10 | except ImportError as exc: # pragma: no cover 11 | raise ImportError("Package 'Django' is required to use this module.") from exc 12 | 13 | from typing import Mapping, Type 14 | 15 | import django.db.models 16 | import rest_framework.serializers 17 | 18 | import cl_sii.extras.dj_model_fields 19 | import cl_sii.extras.drf_fields 20 | 21 | 22 | model_serializer_field_mapping: Mapping[ 23 | Type[django.db.models.Field], Type[rest_framework.serializers.Field] 24 | ] 25 | """ 26 | Mapping of Django model fields to DRF serializer fields. 27 | 28 | Use this to extend DRF serializers that inherit from :class:`ModelSerializer` so 29 | that Django model fields from :mod:`cl_sii.extras.dj_model_fields` do not have 30 | to be explicitly defined in the serializer. 31 | 32 | Usage example: 33 | 34 | >>> class ExampleSerializer(rest_framework.serializers.ModelSerializer): 35 | ... serializer_field_mapping = { 36 | ... **rest_framework.serializers.ModelSerializer.serializer_field_mapping, 37 | ... **model_serializer_field_mapping, 38 | ... } 39 | """ 40 | model_serializer_field_mapping = { 41 | cl_sii.extras.dj_model_fields.RutField: cl_sii.extras.drf_fields.RutField, 42 | } 43 | -------------------------------------------------------------------------------- /src/cl_sii/libs/json_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Mapping 4 | 5 | 6 | ############################################################################### 7 | # exceptions 8 | ############################################################################### 9 | 10 | 11 | class JsonSchemaValidationError(Exception): 12 | """ 13 | JSON data failed validation against a schema. 14 | 15 | Note: For the purpose of using this exception, anything that performs 16 | schema-like validations against JSON data is considered a 'schema', even 17 | if it is not a standard 'JSON Schema'. 18 | """ 19 | 20 | 21 | ############################################################################### 22 | # functions 23 | ############################################################################### 24 | 25 | 26 | def read_json_schema(file_path: Path) -> Mapping[str, object]: 27 | """ 28 | Instantiate an JSON schema object from a file. 29 | 30 | .. warning:: It is assumed that the schema is valid, and providing an 31 | invalid schema can lead to undefined behavior. 32 | 33 | :raises FileNotFoundError: If there is no file at ``file_path``. 34 | """ 35 | with file_path.open(mode='rb') as file: 36 | content = json.load(file) 37 | 38 | if isinstance(content, Mapping): 39 | return content 40 | else: 41 | raise TypeError( 42 | f"Expected JSON file content to be a 'Mapping', not a '{content.__class__.__name__}'.", 43 | ) 44 | -------------------------------------------------------------------------------- /src/tests/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Mapping 4 | 5 | 6 | _TESTS_DIR_PATH = os.path.dirname(__file__) 7 | 8 | 9 | def get_test_file_path(path: str) -> str: 10 | filepath = os.path.join( 11 | _TESTS_DIR_PATH, 12 | path, 13 | ) 14 | return filepath 15 | 16 | 17 | def read_test_file_bytes(path: str) -> bytes: 18 | filepath = os.path.join( 19 | _TESTS_DIR_PATH, 20 | path, 21 | ) 22 | with open(filepath, mode='rb') as file: 23 | content = file.read() 24 | 25 | return content 26 | 27 | 28 | def read_test_file_str_ascii(path: str) -> str: 29 | filepath = os.path.join( 30 | _TESTS_DIR_PATH, 31 | path, 32 | ) 33 | with open(filepath, mode='rt', encoding='ascii') as file: 34 | content = file.read() 35 | 36 | return content 37 | 38 | 39 | def read_test_file_str_utf8(path: str) -> str: 40 | filepath = os.path.join( 41 | _TESTS_DIR_PATH, 42 | path, 43 | ) 44 | with open(filepath, mode='rt', encoding='utf8') as file: 45 | content = file.read() 46 | 47 | return content 48 | 49 | 50 | def read_test_file_json_dict(path: str) -> Mapping[str, object]: 51 | filepath = os.path.join( 52 | _TESTS_DIR_PATH, 53 | path, 54 | ) 55 | with open(filepath, mode='rb') as file: 56 | content = json.load(file) 57 | 58 | if isinstance(content, Mapping): 59 | return content 60 | else: 61 | raise TypeError( 62 | f"Expected JSON file content to be a 'Mapping', not a '{content.__class__.__name__}'.", 63 | ) 64 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/prueba-sii-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEPjCCA6mgAwIBAgIDAgGKMAsGCSqGSIb3DQEBBDCBsTEdMBsGA1UECBQUUmVn 3 | aW9uIE1ldHJvcG9saXRhbmExETAPBgNVBAcUCFNhbnRpYWdvMSIwIAYDVQQDFBlF 4 | LUNlcnRjaGlsZSBDQSBJbnRlcm1lZGlhMTYwNAYDVQQLFC1FbXByZXNhIE5hY2lv 5 | bmFsIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExFDASBgNVBAoUC0UtQ0VS 6 | VENISUxFMQswCQYDVQQGEwJDTDAeFw0wMjEwMDIxOTExNTlaFw0wMzEwMDIwMDAw 7 | MDBaMIHXMR0wGwYDVQQIFBRSZWdpb24gTWV0cm9wb2xpdGFuYTEnMCUGA1UECxQe 8 | U2VydmljaW8gZGUgSW1wdWVzdG9zIEludGVybm9zMScwJQYDVQQKFB5TZXJ2aWNp 9 | byBkZSBJbXB1ZXN0b3MgSW50ZXJub3MxETAPBgNVBAcUCFNhbnRpYWdvMR8wHQYJ 10 | KoZIhvcNAQkBFhB3Z29uemFsZXpAc2lpLmNsMSMwIQYDVQQDFBpXaWxpYmFsZG8g 11 | R29uemFsZXogQ2FicmVyYTELMAkGA1UEBhMCQ0wwXDANBgkqhkiG9w0BAQEFAANL 12 | ADBIAkEAvNQyaLPd3cQlBr0fQWooAKXSFan/WbaFtD5P7QDzcE1pBIvKY2Uv6uid 13 | ur/mGVB9IS4Fq/1xRIXy13FFmxLwTQIDAQABo4IBgjCCAX4wIwYDVR0RBBwwGqAY 14 | BggrBgEEAcNSAaAMFgowNzg4MDQ0Mi00MDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6 15 | Ly9jcmwuZS1jZXJ0Y2hpbGUuY2wvRWNlcnRjaGlsZUNBSS5jcmwwIwYDVR0SBBww 16 | GqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIHmBgNVHSAEgd4wgdswgdgGCCsG 17 | AQQBw1IAMIHLMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmUtY2VydGNoaWxlLmNs 18 | L3BvbGl0aWNhL2Nwcy5odG0wgZAGCCsGAQUFBwICMIGDGoGARWwgdGl0dWxhciBo 19 | YSBzaWRvIHZhbGlkYWRvIGVuIGZvcm1hIHByZXNlbmNpYWwsIHF1ZWRhbmRvIGhh 20 | YmlsaXRhZG8gZWwgQ2VydGlmaWNhZG8gcGFyYSB1c28gdHJpYnV0YXJpbywgcGFn 21 | b3MsIGNvbWVyY2lvIHUgb3Ryb3MwCwYDVR0PBAQDAgTwMAsGCSqGSIb3DQEBBAOB 22 | gQB2V4cTj7jo1RawmsRQUSnnvJjMCrZstcHY+Ss3IghVPO9eGoYzu5Q63vzt0Pi8 23 | CS91SBc7xo+LDoljaUyjOzj7zvU7TpWoFndiTQF3aCOtTkV+vjCMWW3sVHes4UCM 24 | DkF3VYK+rDTAadiaeDArTwsx4eNEpxFuA/TJwcXpLQRCDg== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | platform = linux 4 | mypy_path = 5 | src 6 | files = 7 | *.py, 8 | src 9 | exclude = (?x)( 10 | ^src/tests/( # Tests 11 | test_cte_f29_data_models 12 | | test_dte_data_models 13 | | test_dte_parse 14 | | test_extras_dj_filters 15 | | test_extras_dj_form_fields 16 | | test_extras_dj_model_fields 17 | | test_extras_mm_fields 18 | | test_libs_crypto_utils 19 | | test_libs_dataclass_utils 20 | | test_libs_io_utils 21 | | test_libs_tz_utils 22 | | test_libs_xml_utils 23 | | test_rtc_data_models 24 | | test_rtc_data_models_aec 25 | | test_rtc_data_models_cesiones_periodo 26 | | test_rtc_xml_utils 27 | | test_rut 28 | | test_rut_crypto_utils 29 | )\.py$ 30 | | ^src/tests/( # Test object factories 31 | cte_f29_factories 32 | )\.py$ 33 | ) 34 | plugins = 35 | pydantic.mypy 36 | 37 | follow_imports = normal 38 | ignore_missing_imports = False 39 | no_implicit_reexport = True 40 | strict_optional = True 41 | disallow_untyped_defs = True 42 | check_untyped_defs = True 43 | warn_return_any = True 44 | warn_unused_ignores = True 45 | 46 | show_column_numbers = True 47 | show_error_codes = True 48 | show_error_context = True 49 | error_summary = True 50 | 51 | [mypy-defusedxml.*] 52 | ignore_missing_imports = True 53 | 54 | [mypy-django.*] 55 | ignore_missing_imports = True 56 | 57 | [mypy-django_filters.*] 58 | ignore_missing_imports = True 59 | 60 | [mypy-rest_framework.*] 61 | ignore_missing_imports = True 62 | 63 | [pydantic-mypy] 64 | init_forbid_extra = True 65 | init_typed = True 66 | warn_required_dynamic_aliases = True 67 | warn_untyped_fields = True 68 | -------------------------------------------------------------------------------- /src/cl_sii/extras/dj_url_converters.py: -------------------------------------------------------------------------------- 1 | """ 2 | cl_sii "extras" / Django URL converters. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from typing import ClassVar 8 | 9 | import cl_sii.dte.constants 10 | import cl_sii.rut 11 | 12 | 13 | class RutConverter: 14 | """ 15 | Django URL path converter for Chilean RUT. 16 | 17 | Thousands separators are not supported. 18 | 19 | Example: 20 | 21 | >>> from django.urls import path, register_converter 22 | >>> register_converter(RutConverter, 'cl_sii_rut') 23 | >>> urlpatterns = [path('example//', ...)] 24 | 25 | .. seealso:: 26 | https://docs.djangoproject.com/en/4.2/topics/http/urls/#registering-custom-path-converters 27 | """ 28 | 29 | regex: ClassVar[str] = r'\d{1,8}-[\dKk]' 30 | 31 | def to_python(self, value: str) -> cl_sii.rut.Rut: 32 | return cl_sii.rut.Rut(value) 33 | 34 | def to_url(self, value: cl_sii.rut.Rut) -> str: 35 | return str(value) 36 | 37 | 38 | class TipoDteConverter: 39 | """ 40 | Django URL path converter for `Tipo DTE` object. 41 | 42 | Example: 43 | 44 | >>> from django.urls import path, register_converter 45 | >>> register_converter(TipoDteConverter, 'cl_sii_tipo_dte') 46 | >>> urlpatterns = [path('example//', ...)] 47 | 48 | .. seealso:: 49 | https://docs.djangoproject.com/en/4.2/topics/http/urls/#registering-custom-path-converters 50 | """ 51 | 52 | regex: ClassVar[str] = r'\d{2,3}' 53 | 54 | def to_python(self, value: str) -> cl_sii.dte.constants.TipoDte: 55 | return cl_sii.dte.constants.TipoDte(int(value)) 56 | 57 | def to_url(self, value: cl_sii.dte.constants.TipoDte) -> str: 58 | return str(value.value) 59 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/TEST-DTE-13185095-K.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJdYOEZnh0gmB5 3 | hUfDzS/5oq9u0CXZ+xFozZyw+27R0frwlUioe9Xyyhzx1PyUsp3OLddI18zxLf3s 4 | 0ZP9KdoQd43P90P+oVqkQkQgt9fCabWU7SFKZEXZXAi36ubVvuOA/MgKKrcny59w 5 | elucNoP0CbBVElLMDBIjdF6eoXykZI4LsHdU5cQ8SDGC3qmtPTl7oikZ6lGTrNXO 6 | egoMP/rz6b6O9MJ6CdDmLCgI3zzuTlYdScXv2nNz/p424liRNXurw/5k9ouLHhb0 7 | j25IQV+jdz2XVvyNKSPZBINxeU8ojzrW/8y8+9lNOLUDjvA257h234YY+7nEmDy6 8 | JecHLQKlAgMBAAECggEABne3WTDQ/SySXFRjEW4s9B688xnLnUvqKysutJ/d1u6e 9 | 18pzIrWXEMxcUYc89KknV88w8i27bqLDXC7+SUpmrdCoxNxzWmFjv5JBDavZSWyL 10 | X9SdFP5TH79MqFrqPkJ6m1GCOpFUf/qRi9LhzgoSAmutNY35CoP4sRqzTvRwQ/bH 11 | 4JR2mO1GD3mDvPwUpsONucujuQCpNhalgLCf2OQIG6nfHU1koJawSps8dHvqjf/g 12 | K8x37MtE/vF+ubdyFVRkx6wv3YCaieP4lac9sOrPu7X9dtYDli8yCjJ6waILRilI 13 | 4KXL/bu+hNIw3entuB8V5V5uPP4PrQwZ43VwuabrAQKBgQDZ9Buo5B739GpL468O 14 | ORKrHOPT1j1BOQ2Wz0V3+SbgM1CnRJ19QWIAxqLv8jRWd2o9QWm0UaEr4MpyIo2C 15 | ZMYsIL0ALz9i39WXumwWziIiCpC5ABYt882YZX4nzhJDgt01MnVvLJzCb8J/oJdO 16 | /un3/8maq1nVHNtdhsM9BbeNDQKBgQDsoEzgR2xlf2bA0NnAGKSoACi74NS0COAG 17 | nxF2oq/bhPsQk8UHK5ka6otHsl0lgFRCRG+tNLWnq7jWB+ZhwoVxHeQB5ddGil6V 18 | atQXNuW7V/Xy+CsZCe5/mekKWNdcacOo76cAqbYtLyPAkVl/381S53jEE47Us+6k 19 | 2eptxC3V+QKBgEJp0eva51zjC2jojjUlSvz9JqcsRyoSuoNT0XVHZIM438C4dczv 20 | GW/nF0tKYIxgguz7e7xIi3YVX1r8EGbFUmWr7CucOhJk5m7/jWQ9l8ULtyHIVvnV 21 | qrZfZtu2PXZ47/L/1yzzSSkuaPP++VxG7QB23vXUdOEtk+Kh5+g2T8IZAoGAAPij 22 | eCQy6LO+KzpwOl6fhmUBxculc9u5d619d9wxFpiUIzxICcB/D2I5EiFESpwdPGxl 23 | fPODb13AE3jS1EHlJFK4Fd3opUx6GOjoV/QMu1kgFFA6dQ7aYMGz+CvnLmTsvavG 24 | JrWLnuHbprWyBVlY0WdL0po18t+OMjUGxk6Q1ZkCgYEAznH4XiQo/MHfcSkvUunm 25 | 8Hn5LI+aP+kbIg/NExrOQR3mbQaXhpAJzb4+VRX/l5XNvJ2AHv64lSKTgirvK+p7 26 | jmB8+pPC3XZ9nyfWYBj4+GMudVytlbDb0Sxrr9AZK3GQaVW26WBffWALAmClWFFm 27 | bkGfTLjxBHBG6vqhFBGF/Ak= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/TEST-DTE-WITH-ID-BUT-NO-RUT.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCcsPQO/KxT8WNj 3 | /pXk9japl0voyDhyN3bNqgqVgWL2jwzllR1SQRw7ja0T4uoFhWf3O3NZyGrnLV7a 4 | K4Rj0IYrMywM6DoEuHW/R0iwO8PPK8JpENg9245GaNB45rICcm1nBBGH46ZhRa+J 5 | VdtBwqFLvqbzbCgFseH10evfcNT2QWRvpXDdkSjejNtSDnFVXeaUNQRa8N2xgnBF 6 | tmhVakMXsqa/RKwr320c1ksO2ukIz8x9xQPtExppSOpGbip4NqhXbEA6yAZQvyzu 7 | UGY3GhA37rTf9ka3HYt4XTz8xokVV21Zj8IceolGUl4vbVLhCFM0aY20qEfp+4Oh 8 | JIs/n3SFAgMBAAECggEAAoNvIsdoTz9lv/6fMmlFprJD2DPP/fsIR5PE8DF/YCOa 9 | yhr6ea2MMaNb5aAD73s8l/Fm8AeAOX2XkinVCZHYeRxsxjc6aQV5dAxFbPhEc5AI 10 | 4g0QXuuk7Fm1kF7o95OU0Cx3SIX9Dv3iazJKnlMsKa4g+PIg8ThxfrMzlKW3cMzE 11 | zfWdgGe65I0cOS8kLi7C+nd23y2EMn/Zfq55pguSYq0E49A+J30KzRFmZZWiXsfB 12 | U0Q1mOHEl4Lbl0WLeJnjp+adDTgRwUMoAsuQ87SGr+8BRwZCJX2wLmdBS4NKvZFH 13 | Xsxu4OQxxx8SySxeGKqmyE4cUAL2iCy18vS1wL/5OQKBgQDGjpoe9aU8eEt0vDbe 14 | /SyJITMchnqOUNGEBAyRmPqViGolHd+htnrj3hJ1O44vTjUTxj3NWxon2pFYMrtj 15 | LHBN9CBzt3/1KMjUQmQL1BmIMRlvXS8HsRQl4Nt9lP4GqtX0tbi64JydCENiavL9 16 | zVE0mCV4HzNSqSiTfqEVBZ/NvQKBgQDKBbalPtzQQ+sgvslMQPNpNFCpow4Nn6Eo 17 | CHGQ9nZFFyEpdMxcpqhYwgiy8eosBXTEhbhQyYcbxB77EDH1WLcho1ZII8rX6LrM 18 | 153B/8XuIdEkJDkcYhEGmM0CAO+X8Nztkksk3T3jpG8TFdZBlk2giegBV4ko2Ina 19 | HdkRqah6aQKBgEM6mXiOF+qHmJTn/XQ3KNMtiI7KAckaGDao4FCUCZSD4dy7ZrLs 20 | hGOPF5TWG2htBI+zec2EYTDJUpkYZFZJ/6SFWk+T/CFYM9eauyE+KX7xkPkiBgCG 21 | tpm0rtywi+paAaOfu/Kahqys1ZQHPkstL6etNFKdzdTZLcHzCDuD8f3JAoGATDaj 22 | lOuGOjulNJFFN7M5IPNPiu+smY8jKQsmbN3N+HqlVBJwFnP5BqMMzRVeloTobEtW 23 | IYQlqF/woB6X+kshq1sHbeey2ok+D5E4PrvTW+b+E3hm40JL0gVLMfpQaS3A6w9J 24 | sfqVIpAiJz0Ru2SMnIfqMrdnUzV9q/+eqH8sxCECgYBxIN/8r/H1lmJBa8vaAO5P 25 | Nb6j6yRgWkIIS8lN9uGjs5O3qfT0MDvkiyp6vDetHelKF+ayJ+SeNpuj978g2lhy 26 | KgUtUYeo1qXLHHpEpmf/Dc597v+v8aI9uUD3RFdOtR2yup3JUlDVJFf/VtF27a5X 27 | aC5sPUAoOReOMN6lLnxKrg== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Configuration for Ignoring Revisions in Git Blame 2 | # 3 | # This file contains a list of revisions that are not helpful when assigning blame, such as source 4 | # code reformatting, Python `import` sorting, etc., so that `git blame` is able to ignore those 5 | # commits. 6 | # 7 | # > Ignore changes made by the revision when assigning blame, as if the change never happened. 8 | # > Lines that were changed or added by an ignored commit will be blamed on the previous commit 9 | # > that changed that line or nearby lines. 10 | # 11 | # Documentation: 12 | # - https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt 13 | # - https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view 14 | # 15 | # Using this file with Git Blame: 16 | # - GitHub automatically uses this configuration file. 17 | # - To use locally, run `git config blame.ignoreRevsFile .git-blame-ignore-revs`. 18 | 19 | # Sort Python imports with Isort 20 | 33443511137a819050116358d46484a02e1b1157 21 | 2e839bce77b2a497789cd8db0603b59b34dc2a24 22 | 3e65ad14b0f2e35b6b817be668685682b15594a2 23 | 24 | # Reformat source code using 'Black' 25 | 30c98966eb67bb7155085a0801590d4de5ee27d4 26 | fdad3571586afb8dc94f280572d315434b52b11c 27 | c3453173311d98eb8db9f35067a62bc8a6e296d1 28 | b5d75137c6c3cffbbc12455f02306134871ca7e8 29 | df26fa3d114fe063d3809b75931b78b9472adc98 30 | e4a165f408c9d6a78bc929f379fd5adb5435f824 31 | d9f7e31f8f9a13e739859d0282e36591cec10ec6 32 | a98bf81dfef2ddd0a7d72a3b329aee214a020894 33 | 8429acfe04f0cb88ea528d5c580d397db269fab4 34 | 84c1be060c364c7316b1705ef41283b17f1061dc 35 | c5040e9878dbf62cceb6262a1aa27df72d0c0752 36 | da84feb45852860f549bb982d8f5371e3f5dffa3 37 | 38 | # Reformat source code 39 | 605f6fe7c228401492177c93d5c661a9d2c03c8d 40 | 41 | # Move files without changing them 42 | 347201cc5442962a0bd764df28bd4709e5b3fcd6 43 | -------------------------------------------------------------------------------- /src/cl_sii/libs/csv_utils.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from typing import IO, Optional, Sequence, Type, Union 3 | 4 | 5 | def create_csv_dict_reader( 6 | text_stream: IO[str], 7 | csv_dialect: Type[csv.Dialect], 8 | row_dict_extra_fields_key: Union[str, None] = None, 9 | expected_fields_strict: bool = True, 10 | expected_field_names: Optional[Sequence[str]] = None, 11 | ) -> csv.DictReader: 12 | """ 13 | Create a CSV dict reader with custom options. 14 | 15 | :param text_stream: 16 | :param row_dict_extra_fields_key: 17 | CSV row dict key under which the extra data in the row will be saved 18 | :param csv_dialect: 19 | :param expected_fields_strict: 20 | :param expected_field_names: 21 | (required if ``expected_field_names`` is True) 22 | :return: a CSV DictReader 23 | 24 | """ 25 | csv_reader = csv.DictReader( 26 | text_stream, 27 | fieldnames=None, # the values of the first row will be used as the fieldnames 28 | restkey=row_dict_extra_fields_key, 29 | dialect=csv_dialect, 30 | ) 31 | 32 | if expected_fields_strict: 33 | if expected_field_names: 34 | if csv_reader.fieldnames is None: 35 | raise Exception( 36 | "Programming error: when a 'csv.DictReader' instance is created with" 37 | "'fieldnames=None', the attribute will be set to the values of the first row." 38 | ) 39 | if tuple(csv_reader.fieldnames) != expected_field_names: 40 | raise ValueError( 41 | "CSV file field names do not match those expected, or their order.", 42 | csv_reader.fieldnames, 43 | ) 44 | else: 45 | raise ValueError( 46 | "Param 'expected_field_names' is required if 'expected_fields_strict' is True." 47 | ) 48 | 49 | return csv_reader 50 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rcv/RCV-venta-extra-empty-impuestos-rows.csv: -------------------------------------------------------------------------------- 1 | Nro;Tipo Doc;Tipo Venta;Rut cliente;Razon Social;Folio;Fecha Docto;Fecha Recepcion;Fecha Acuse Recibo;Fecha Reclamo;Monto Exento;Monto Neto;Monto IVA;Monto total;IVA Retenido Total;IVA Retenido Parcial;IVA no retenido;IVA propio;IVA Terceros;RUT Emisor Liquid. Factura;Neto Comision Liquid. Factura;Exento Comision Liquid. Factura;IVA Comision Liquid. Factura;IVA fuera de plazo;Tipo Docto. Referencia;Folio Docto. Referencia;Num. Ident. Receptor Extranjero;Nacionalidad Receptor Extranjero;Credito empresa constructora;Impto. Zona Franca (Ley 18211);Garantia Dep. Envases;Indicador Venta sin Costo;Indicador Servicio Periodico;Monto No facturable;Total Monto Periodo;Venta Pasajes Transporte Nacional;Venta Pasajes Transporte Internacional;Numero Interno;Codigo Sucursal;NCE o NDE sobre Fact. de Compra;Codigo Otro Imp.;Valor Otro Imp.;Tasa Otro Imp. 2 | 1;33;Del Giro;54213736-3;CHILE SPA;6541;01/09/2025;01/09/2025 10:09:00;08/09/2025 14:15:23;;0;7217280;1371283;9565862;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;12354;;27;275904;10;;;;;; 3 | ;33;Del Giro;54213736-3;CHILE SPA;6541;01/09/2025;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;271;701395;18;;;;;; 4 | 2;33;Del Giro;42509414-9;COMERCIAL SPA;9874;01/09/2025;01/09/2025 09:53:17;;;0;8879040;1687018;13136156;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;12354;;24;2570098;31.5 5 | 3;33;Del Giro;68840666-8;TEXAS SPA;3210;01/09/2025;01/09/2025 10:58:51;08/09/2025 14:15:23;;0;20522880;3899347;30471437;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;12354;;24;6049210;31.5; 6 | 4;34;Del Giro;68840666-8;TEXAS SPA;3210;01/09/2025;01/09/2025 10:58:51;08/09/2025 14:15:23;;0;9999;3899347;30471437;2020;;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;12354;;24;6049210;31.5; 7 | 5;33;Del Giro;54213736-3;THE COMPANY SPA;3210;01/09/2025;01/09/2025 10:58:51;08/09/2025 14:15:23;;0;9999;3899347;30471437;0;0;0;0;0;-;0;0;0;0;0;;;;0;;0;2;0;0;0;;;;12354;;24;6049210;31.5; 8 | ;33;Del Giro;54213736-3;THE COMPANY SPA;3210;01/09/2025;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;271;701395;18;;;;;; 9 | -------------------------------------------------------------------------------- /src/cl_sii/libs/encoding_utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import binascii 3 | from typing import Union 4 | 5 | 6 | def clean_base64(value: Union[str, bytes]) -> bytes: 7 | """ 8 | Force bytes and remove line breaks and spaces. 9 | 10 | Does not validate base64 format. 11 | 12 | :raises ValueError: 13 | :raises TypeError: 14 | 15 | """ 16 | if isinstance(value, bytes): 17 | value_base64_bytes = value 18 | elif isinstance(value, str): 19 | try: 20 | value_base64_bytes = value.strip().encode(encoding='ascii', errors='strict') 21 | except UnicodeEncodeError as exc: 22 | raise ValueError("Only ASCII characters are accepted.", str(exc)) from exc 23 | else: 24 | raise TypeError("Value must be str or bytes.") 25 | 26 | # remove line breaks and spaces 27 | # warning: we may only remove characters that are not part of the standard base-64 alphabet 28 | # (or any of its popular alternatives). 29 | value_base64_bytes_cleaned = ( 30 | value_base64_bytes.replace(b'\n', b'') 31 | .replace(b'\r', b'') 32 | .replace(b'\t', b'') 33 | .replace(b' ', b'') 34 | ) 35 | 36 | return value_base64_bytes_cleaned 37 | 38 | 39 | def decode_base64_strict(value: Union[str, bytes]) -> bytes: 40 | """ 41 | Strict conversion for str/bytes, tolerating only line breaks and spaces. 42 | 43 | :raises ValueError: non-base64 input or non-ASCII characters included 44 | 45 | """ 46 | value_base64_bytes_cleaned = clean_base64(value) 47 | try: 48 | value_bytes = base64.b64decode(value_base64_bytes_cleaned, validate=True) 49 | except binascii.Error as exc: 50 | raise ValueError("Input is not a valid base64 value.", str(exc)) from exc 51 | return value_bytes 52 | 53 | 54 | def validate_base64(value: Union[str, bytes]) -> None: 55 | """ 56 | Validate that ``value`` is base64-encoded data. 57 | 58 | :raises ValueError: 59 | :raises TypeError: 60 | 61 | """ 62 | decode_base64_strict(value) 63 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile --allow-unsafe --strip-extras requirements.in 6 | # 7 | annotated-types==0.7.0 8 | # via pydantic 9 | asgiref==3.8.1 10 | # via django 11 | attrs==23.2.0 12 | # via 13 | # jsonschema 14 | # referencing 15 | backports-datetime-fromisoformat==2.0.3 16 | # via marshmallow 17 | certifi==2024.7.4 18 | # via signxml 19 | cffi==1.17.1 20 | # via cryptography 21 | cryptography==45.0.7 22 | # via 23 | # -r requirements.in 24 | # pyopenssl 25 | # signxml 26 | defusedxml==0.7.1 27 | # via -r requirements.in 28 | django==4.2.26 29 | # via 30 | # -r requirements.in 31 | # django-filter 32 | # djangorestframework 33 | django-filter==25.1 34 | # via -r requirements.in 35 | djangorestframework==3.16.0 36 | # via -r requirements.in 37 | importlib-metadata==8.7.0 38 | # via -r requirements.in 39 | jsonschema==4.25.1 40 | # via -r requirements.in 41 | jsonschema-specifications==2023.12.1 42 | # via jsonschema 43 | lxml==6.0.2 44 | # via 45 | # -r requirements.in 46 | # signxml 47 | marshmallow==4.0.1 48 | # via -r requirements.in 49 | pycparser==2.22 50 | # via cffi 51 | pydantic==2.11.7 52 | # via -r requirements.in 53 | pydantic-core==2.33.2 54 | # via pydantic 55 | pyopenssl==25.1.0 56 | # via -r requirements.in 57 | pytz==2025.2 58 | # via -r requirements.in 59 | referencing==0.35.1 60 | # via 61 | # jsonschema 62 | # jsonschema-specifications 63 | rpds-py==0.19.0 64 | # via 65 | # jsonschema 66 | # referencing 67 | signxml==4.2.0 68 | # via -r requirements.in 69 | sqlparse==0.5.0 70 | # via django 71 | typing-extensions==4.15.0 72 | # via 73 | # -r requirements.in 74 | # asgiref 75 | # marshmallow 76 | # pydantic 77 | # pydantic-core 78 | # pyopenssl 79 | # typing-inspection 80 | typing-inspection==0.4.0 81 | # via pydantic 82 | zipp==3.20.2 83 | # via importlib-metadata 84 | 85 | # The following packages are considered to be unsafe in a requirements file: 86 | setuptools==80.9.0 87 | # via -r requirements.in 88 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM 3 | MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g 4 | QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg 5 | LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5 6 | NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ 7 | BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ 8 | QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu 9 | YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw 10 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld 11 | pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo 12 | ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q 13 | f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh 14 | /qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a 15 | h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA 16 | AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud 17 | DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw 18 | FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE 19 | bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw 20 | dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t 21 | IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw 22 | NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv 23 | QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05 24 | oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h 25 | cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw 26 | AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/ 27 | BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny 28 | bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD 29 | CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb 30 | 9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J 31 | gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr 32 | 2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA 33 | NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf 34 | zmrw 35 | -----END CERTIFICATE----- 36 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/AEC--76399752-9--33--25568--SEQ-1-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM 3 | MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g 4 | QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg 5 | LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5 6 | NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ 7 | BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ 8 | QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu 9 | YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw 10 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld 11 | pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo 12 | ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q 13 | f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh 14 | /qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a 15 | h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA 16 | AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud 17 | DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw 18 | FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE 19 | bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw 20 | dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t 21 | IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw 22 | NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv 23 | QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05 24 | oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h 25 | cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw 26 | AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/ 27 | BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny 28 | bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD 29 | CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb 30 | 9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J 31 | gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr 32 | 2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA 33 | NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf 34 | zmrw 35 | -----END CERTIFICATE----- 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for Deployment 2 | 3 | name: Deploy 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | deploy_env: 9 | type: string 10 | required: true 11 | description: Deployment Environment 12 | artifacts_path: 13 | type: string 14 | required: true 15 | 16 | permissions: 17 | contents: read 18 | 19 | # -----BEGIN Environment Variables----- 20 | 21 | # Environment variables required for deployment: 22 | # 23 | # - PYPI_PASSWORD := PyPI password or API token. 24 | # - PYPI_USERNAME := PyPI username. For API tokens, use "__token__". 25 | # - TWINE_NON_INTERACTIVE := Do not interactively prompt for credentials if they are missing. 26 | # - TWINE_REPOSITORY_URL := The repository (package index) URL to register the package to. 27 | 28 | env: 29 | PYTHON_VIRTUALENV_ACTIVATE: venv/bin/activate 30 | 31 | # -----END Environment Variables----- 32 | 33 | jobs: 34 | deploy: 35 | name: Deploy 36 | runs-on: ubuntu-22.04 37 | environment: ${{ inputs.deploy_env }} 38 | 39 | steps: 40 | - name: Check Out VCS Repository 41 | uses: actions/checkout@v6.0.0 42 | 43 | - name: Set Up Python 44 | id: set_up_python 45 | uses: actions/setup-python@v6.1.0 46 | with: 47 | python-version: "3.13" 48 | 49 | - name: Restoring/Saving Cache 50 | uses: actions/cache@v4.3.0 51 | with: 52 | path: "venv" 53 | key: py-v1-deps-${{ runner.os }}-${{ steps.set_up_python.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'requirements.txt', 'requirements-dev.txt', 'Makefile', 'make/**.mk') }} 54 | 55 | - name: Restore Artifacts (Release) 56 | uses: actions/download-artifact@v6.0.0 57 | with: 58 | name: release 59 | path: ${{ inputs.artifacts_path }}/ 60 | 61 | - name: Deploy 62 | run: | 63 | source "$PYTHON_VIRTUALENV_ACTIVATE" 64 | make deploy \ 65 | TWINE_USERNAME="${PYPI_USERNAME:?}" \ 66 | TWINE_PASSWORD="${PYPI_PASSWORD:?}" 67 | env: 68 | PYPI_USERNAME: ${{ vars.PYPI_USERNAME }} 69 | PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 70 | TWINE_NON_INTERACTIVE: "true" 71 | TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ 72 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--96670340-7--61--110616-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGOjCCBSKgAwIBAgIKFh8ihAAAAAuKQDANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx 3 | HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE 4 | ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD 5 | EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW 6 | GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xODA3MTQyMjA3MzdaFw0yMTA3MTMyMjA3Mzda 7 | MIG7MQswCQYDVQQGEwJDTDEjMCEGA1UECBMaTUVUUk9QT0xJVEFOQSBERSBTQU5USUFHTyAxETAP 8 | BgNVBAcTCFNhbnRpYWdvMSAwHgYDVQQKExdCbHVldGVjaCBDb25zdWx0aW5nIFNwQTEKMAgGA1UE 9 | CwwBKjEiMCAGA1UEAxMZTWFyaWEgVGVyZXNhIEFiYXN0byBQZXJlejEiMCAGCSqGSIb3DQEJARYT 10 | c29wb3J0ZUBibHVlbGluZS5jbDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAutp/IHvu2WqN 11 | +rq146FGZ9HLMRhaQVyokp6fkwvOu6mgxIWvrEClE4cuZG3N5q6eT6Dpldg4ulf9j4ioZ/tYZ119 12 | +hCN3SdNK9+BlJPqH+Yz3pzU1PRLyResbxhLM2HBPC5NhfqdYaH3RaMYifE6YvJkxElJ3ORstJEX 13 | zS7TsBkCAwEAAaOCAqkwggKlMIIBTwYDVR0gBIIBRjCCAUIwggE+BggrBgEEAcNSBTCCATAwLQYI 14 | KwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUuY2wvQ1BTLmh0bTCB/gYIKwYBBQUHAgIw 15 | gfEege4ARQBsACAAcgBlAHMAcABvAG4AZABlAHIAIABlAHMAdABlACAAZgBvAHIAbQB1AGwAYQBy 16 | AGkAbwAgAGUAcwAgAHUAbgAgAHIAZQBxAHUAaQBzAGkAdABvACAAaQBuAGQAaQBzAHAAZQBuAHMA 17 | YQBiAGwAZQAgAHAAYQByAGEAIABkAGEAcgAgAGkAbgBpAGMAaQBvACAAYQBsACAAcAByAG8AYwBl 18 | AHMAbwAgAGQAZQAgAGMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4ALgAgAFAAbwBzAHQAZQByAGkA 19 | bwByAG0AZQBuAHQAZQAsMB0GA1UdDgQWBBRhyzEF5P9e+/nyySWp9aqz3051zTALBgNVHQ8EBAMC 20 | BPAwIwYDVR0RBBwwGqAYBggrBgEEAcEBAaAMFgoyMzA5OTUzNi0yMB8GA1UdIwQYMBaAFHjhPp/S 21 | ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu 22 | Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 23 | Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDA9BgkrBgEEAYI3FQcEMDAuBiYrBgEEAYI3FQiC3IMv 24 | hZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAjBgNVHRIEHDAaoBgGCCsGAQQBwQECoAwW 25 | Cjk2OTI4MTgwLTUwDQYJKoZIhvcNAQEFBQADggEBAFf1p3ZXYrCCbcUzvPAajwmuI8JjFgtwZ2OF 26 | On62xMjX7Tj19n8Ad2ephI6d/vysjfpjR1QX2HzYGtnigX1eG0zSSkMXNw12hGl0Oyja2iIieHlC 27 | nJbaP0uQaaZnGvi5kpas6VUQAcD02sVMG4QA+iiGAhOYEV58+YwtkDnzNrdYJJMEE5qqzIBT0Uv9 28 | 9MdD6f/zoeQQ9adEI1Hshe/c4fp7P4A5vUVTfAWitH0UVMVSUG5FHz806gMhjMWFu029mQE8PH0t 29 | IUWykHq2GsoSg3fT8clr+luOsU1JwFKUY51+P4E4Y+QNWckEedLfeYMSu+vXtEABfb36+XAox5gU 30 | 7NU= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/howto.md: -------------------------------------------------------------------------------- 1 | # Generating a self-signed certificate in DER format using OpenSSL 2 | 3 | ## Documentation 4 | 5 | - 6 | 7 | ## Parameters 8 | 9 | - File to send the key to (`key_file_name`) 10 | - Output file (`certificate_file_name`) 11 | - Number of days cert is valid for (`number_of_days`) 12 | 13 | ```sh 14 | key_file_name='key.pem' 15 | certificate_file_name='certificate.der' 16 | number_of_days=365 17 | subject_rut_oid='1.3.6.1.4.1.8321.1' 18 | subject_rut='13185095-K' 19 | ``` 20 | 21 | ## Steps 22 | 23 | ### Generate the private key and public certificate 24 | 25 | ```sh 26 | openssl req \ 27 | -newkey rsa:2048 \ 28 | -nodes \ 29 | -keyout "$key_file_name" \ 30 | -x509 \ 31 | -days "$number_of_days" \ 32 | -outform DER \ 33 | -out "$certificate_file_name" \ 34 | -extensions san -config <(cat /etc/ssl/openssl.cnf \ 35 | <(printf "\n[san]\nsubjectAltName=otherName:$subject_rut_oid;UTF8:$subject_rut")) 36 | ``` 37 | 38 | ```text 39 | Generating a RSA private key 40 | ....................................................................................+++++ 41 | ....................................................+++++ 42 | writing new private key to 'key.pem' 43 | ----- 44 | You are about to be asked to enter information that will be incorporated 45 | into your certificate request. 46 | What you are about to enter is what is called a Distinguished Name or a DN. 47 | There are quite a few fields but you can leave some blank 48 | For some fields there will be a default value, 49 | If you enter '.', the field will be left blank. 50 | ----- 51 | Country Name (2 letter code) [AU]:CL 52 | State or Province Name (full name) [Some-State]:Region Metropolitana 53 | Locality Name (eg, city) []:Santiago 54 | Organization Name (eg, company) [Internet Widgits Pty Ltd]:Acme Corporation 55 | Organizational Unit Name (eg, section) []:Acme Explosive Tennis Balls 56 | Common Name (e.g. server FQDN or YOUR name) []:John Doe 57 | Email Address []:john.doe@acme.com 58 | ``` 59 | 60 | ### Output 61 | 62 | #### Review the created certificate 63 | 64 | ```sh 65 | openssl x509 \ 66 | -inform DER \ 67 | -in "$certificate_file_name" \ 68 | -text -noout 69 | ``` 70 | 71 | This will generate a self-signed certificate in DER format and allow you to review its contents 72 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--60910000-1--33--2336600-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGRDCCBSygAwIBAgIIWkXZU0hXELYwDQYJKoZIhvcNAQELBQAwga8xCzAJBgNVBAYTAkNMMRQw 3 | EgYDVQQKDAtFLVNpZ24gUy5BLjE5MDcGA1UECwwwVGVybXMgb2YgdXNlIGF0IHd3dy5lc2lnbi1s 4 | YS5jb20vYWN1ZXJkb3RlcmNlcm9zMSswKQYDVQQDDCJFLVNpZ24gQ2xhc3MgMiBGaXJtYSBUcmli 5 | dXRhcmlhIENBMSIwIAYJKoZIhvcNAQkBFhNlLXNpZ25AZXNpZ24tbGEuY29tMB4XDTE4MDkwNjIx 6 | MTMwMFoXDTE5MDkwNjIxMTMwMFowgaQxCzAJBgNVBAYTAkNMMRQwEgYDVQQKDAtFLVNpZ24gUy5B 7 | LjE5MDcGA1UECwwwVGVybXMgb2YgdXNlIGF0IHd3dy5lc2lnbi1sYS5jb20vYWN1ZXJkb3RlcmNl 8 | cm9zMSQwIgYDVQQDDBtKb3JnZSBFbnJpcXVlIENhYmVsbG8gT3J0aXoxHjAcBgkqhkiG9w0BCQEW 9 | D2pjYWJlbGxvQG5pYy5jbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKmcXnSacIfT 10 | t/388/jyvIzdQXaCzHnC8RZg/llCBLPPIJCfMXwD4oc00dJQasWvQU36h9ApTS4N2Mn7j/PBGnre 11 | x0VvyjTlqPfA6c4LuLloCuEUyNrmY7rHOyhwE06YXM68lFUpD/42oNdhluLs30dHLA/j18lGaYYX 12 | a5LPlN8nAMVkQqXgnXccmzb3x0DspB5xuXlumw1cAnhCP5v0uiPW/Gds4iRaxYW6h0uSAUsECpl2 13 | 7/7fYDGEN8vM+Zj7Ddis3ZlRr38SA03JjasduV+cHHSKCOL9Nf5pPS05pjysLYqc/daHGKBUkdHP 14 | 4zFCQKRHtuhsSqoV8cfwKkgZcJsCAwEAAaOCAmswggJnMIGABggrBgEFBQcBAQR0MHIwSgYIKwYB 15 | BQUHMAKGPmh0dHA6Ly9wa2kuZXNpZ24tbGEuY29tL2NhY2VydHMvcGtpQ2xhc3MyRmlybWFUcmli 16 | dXRhcmlhQ0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5lc2lnbi1sYS5jb20wHQYDVR0O 17 | BBYEFOn+RHqRCvBA8p2GtOJM9vodB1vHMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU+Ur6wsdu 18 | wucSnFdFNYQabSjpSqQwgcgGA1UdIASBwDCBvTCBugYMKwYBBAGCymoBBAECMIGpMH4GCCsGAQUF 19 | BwICMHIecABDAGUAcgB0AGkAZgBpAGMAYQBkAG8AIABwAGEAcgBhACAAdQBzAG8AIABUAHIAaQBi 20 | AHUAdABhAHIAaQBvACwAIABDAG8AbQBlAHIAYwBpAG8ALAAgAFAAYQBnAG8AcwAgAHkAIABPAHQA 21 | cgBvAHMwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZXNpZ24tbGEuY29tL2NwczBRBgNVHR8ESjBI 22 | MEagRKBChkBodHRwOi8vcGtpLmVzaWduLWxhLmNvbS9jcmwvcGtpQ2xhc3MyRmlybWFUcmlidXRh 23 | cmlhL2VuZHVzZXIuY3JsMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYB 24 | BQUHAwQwIgYDVR0RBBswGaAXBggrBgEEAcEBAaALFgk4NDgwNDM3LTEwIwYDVR0SBBwwGqAYBggr 25 | BgEEAcEBAqAMFgo5OTU1MTc0MC1LMA0GCSqGSIb3DQEBCwUAA4IBAQCHTKFxdzPqCMQTGSBeLQwX 26 | 8qUOB4ZXhwspFSLngua5wSpGL5/ByjeJQoeBMrzN9mduhcgV3sdrK+r61dyWmeGOFxA2/Rsv1D3V 27 | /sLUyLukGxBjn2zydQAdWHlqFB1db0WGjrUFbECbOkaeXnhDEk9RTVWT0SwZpVUKDuq2utyxWGs+ 28 | eCEjjXTJcIgv9C5Sr711JJT0ybyTRocC3dTLc0SCHPpAbIU8jbwZsJIwlitBXED5T6XMA/D6PA/Y 29 | bVRedg3GR8eIunz1G129oMVI3yeiN8jKobnJVl7RTto17o1ar7MuuYLCGwOYJJZZ8ivrzLNuZwjU 30 | fU939flUCn3fX1PY 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/AEC--76354771-K--33--170--SEQ-2-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGNzCCBR+gAwIBAgIKYRvSawAAAAhSHTANBgkqhkiG9w0BAQUFADCB0jELMAkG 3 | A1UEBhMCQ0wxHTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQH 4 | EwhTYW50aWFnbzEUMBIGA1UEChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9y 5 | aWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQDEydFLUNFUlRDSElMRSBDQSBGSVJN 6 | QSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEWGHNjbGllbnRlc0Bl 7 | LWNlcnRjaGlsZS5jbDAeFw0xNzA3MDcxNTAxMTVaFw0xOTA3MDcxNTAxMTVaMIG6 8 | MQswCQYDVQQGEwJDTDEjMCEGA1UECBMaTUVUUk9QT0xJVEFOQSBERSBTQU5USUFH 9 | TyAxETAPBgNVBAcTCFNhbnRpYWdvMRgwFgYDVQQKEw9TVCBDQVBJVEFMIFMuQS4x 10 | EjAQBgNVBAsTCUZBQ1RPUklORzEbMBkGA1UEAxMSQU5EUkVTICBQUkFUUyBWSUFM 11 | MSgwJgYJKoZIhvcNAQkBFhlwZ2FsdmV6bXVub3pAc3RjYXBpdGFsLmNsMIGfMA0G 12 | CSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpu1tU5icGv1Hgv47XxH/VF3GJf6HEEg1q 13 | aAkqpRzOPfF9sv83Uo230yP5Ds5bbF8KZn0v+1aYi4u16pfVxKWgfoI8jbHBBUbk 14 | HmlT8ZCXT8ILe+yGNaJj2xaz+oCBeZFCJkIKmQ4geTbk9z2l03sF/KhxYNIghbWc 15 | s/2B5CwTLwIDAQABo4ICpzCCAqMwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUI 16 | gtyDL4WTjGaF1Z0XguLcJ4Hv7DxhhNWJYoWMtRECAWQCAQQwHQYDVR0OBBYEFKQ6 17 | r6OId16o1Wd37wqvn1k9IFPyMAsGA1UdDwQEAwIE8DAfBgNVHSMEGDAWgBR44T6f 18 | 0hKzejyNzTAOU7NDKQezVTA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY3JsLmUt 19 | Y2VydGNoaWxlLmNsL2VjZXJ0Y2hpbGVjYUZFUy5jcmwwOgYIKwYBBQUHAQEELjAs 20 | MCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5lY2VydGNoaWxlLmNsL29jc3AwIwYD 21 | VR0RBBwwGqAYBggrBgEEAcEBAaAMFgoxNjM2MDM3OS05MCMGA1UdEgQcMBqgGAYI 22 | KwYBBAHBAQKgDBYKOTY5MjgxODAtNTCCAU0GA1UdIASCAUQwggFAMIIBPAYIKwYB 23 | BAHDUgUwggEuMC0GCCsGAQUFBwIBFiFodHRwOi8vd3d3LmUtY2VydGNoaWxlLmNs 24 | L0NQUy5odG0wgfwGCCsGAQUFBwICMIHvHoHsAEMAZQByAHQAaQBmAGkAYwBhAGQA 25 | bwAgAEYAaQByAG0AYQAgAFMAaQBtAHAAbABlAC4AIABIAGEAIABzAGkAZABvACAA 26 | dgBhAGwAaQBkAGEAZABvACAAZQBuACAAZgBvAHIAbQBhACAAcAByAGUAcwBlAG4A 27 | YwBpAGEAbAAsACAAcQB1AGUAZABhAG4AZABvACAAaABhAGIAaQBsAGkAdABhAGQA 28 | bwAgAGUAbAAgAEMAZQByAHQAaQBmAGkAYwBhAGQAbwAgAHAAYQByAGEAIAB1AHMA 29 | bwAgAHQAcgBpAGIAdQB0AGEAcgBpAG8wDQYJKoZIhvcNAQEFBQADggEBAGGMlOaf 30 | oMAlJxsg3e+vvALtwz+AJ/YXR5X7mRWlA3glkGu75rMTj5JSTuc7PbxkxTccR31v 31 | MxxhCISMLKtwRpYrifdTc5RzHCFgZUlgR+PLW+U9Wcwxnp1dX3IjG0XgdG2FY0Ov 32 | mzT38gBe8lhtDfMPQa//1FyyVOHAsz2l9SpTfEtDVX0AG6RV/hJl7VJcZl6mtsGi 33 | PjCWGRvjAXA9Y8+FzTvHRU8JNcGrBMnH7CmnVTOQMbgm4ZlZTvV9EdzX/7eIGD5b 34 | IvvnXRQM/S7oRnHl+81nEgTSYDDgwU3E625am3LMrN09786qEx5VhoU8pL57MWko 35 | f28uFPdpvzjgEG4= 36 | -----END CERTIFICATE----- 37 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx 3 | HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE 4 | ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD 5 | EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW 6 | GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa 7 | MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh 8 | MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE 9 | CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM 10 | b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI 11 | hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx 12 | O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC 13 | /z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 14 | FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV 15 | HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S 16 | ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu 17 | Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 18 | Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 19 | MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC 20 | AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu 21 | Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp 22 | AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A 23 | IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk 24 | AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA 25 | cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA 26 | mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD 27 | qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un 28 | +/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud 29 | B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY 30 | +kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for Continuous Integration and Continuous Delivery 2 | # 3 | # Documentation: 4 | # - https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions 5 | # - https://docs.github.com/en/actions/learn-github-actions/contexts 6 | # - https://docs.github.com/en/actions/learn-github-actions/expressions 7 | # - https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 8 | # - https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts 9 | # - https://docs.github.com/en/actions/using-workflows/reusing-workflows 10 | 11 | name: CI/CD 12 | 13 | on: 14 | push: 15 | 16 | permissions: 17 | contents: write 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.ref }} 21 | cancel-in-progress: true 22 | 23 | env: 24 | PRODUCTION_VCS_REF: refs/heads/master 25 | STAGING_VCS_REF: refs/heads/develop 26 | 27 | jobs: 28 | # -----BEGIN Workflow Configuration Job----- 29 | workflow_config: 30 | name: Workflow Configuration 31 | runs-on: ubuntu-22.04 32 | 33 | outputs: 34 | PRODUCTION_VCS_REF: ${{ env.PRODUCTION_VCS_REF }} 35 | STAGING_VCS_REF: ${{ env.STAGING_VCS_REF }} 36 | 37 | steps: 38 | - run: "true" 39 | 40 | # -----END Workflow Configuration Job----- 41 | 42 | # -----BEGIN CI Job----- 43 | ci: 44 | name: CI 45 | needs: 46 | - workflow_config 47 | 48 | uses: ./.github/workflows/ci.yaml 49 | secrets: inherit 50 | 51 | # -----END CI Job----- 52 | 53 | # -----BEGIN Release Job----- 54 | release: 55 | name: Release 56 | if: ${{ github.ref == needs.workflow_config.outputs.PRODUCTION_VCS_REF }} 57 | needs: 58 | - ci 59 | - workflow_config 60 | 61 | uses: ./.github/workflows/release.yaml 62 | with: 63 | create_git_tag_and_github_release: ${{ github.ref == needs.workflow_config.outputs.PRODUCTION_VCS_REF }} 64 | 65 | # -----END Release Job----- 66 | 67 | # -----BEGIN Deploy Job----- 68 | deploy: 69 | name: Deploy 70 | if: ${{ github.ref == needs.workflow_config.outputs.PRODUCTION_VCS_REF }} 71 | needs: 72 | - release 73 | - workflow_config 74 | 75 | uses: ./.github/workflows/deploy.yaml 76 | with: 77 | deploy_env: prod 78 | artifacts_path: ${{ needs.release.outputs.artifacts_path }} 79 | secrets: inherit 80 | 81 | # -----END Deploy Job----- 82 | -------------------------------------------------------------------------------- /src/cl_sii/extras/drf_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | cl_sii "extras" / Django REST Framework (DRF) fields. 3 | 4 | (for serializers) 5 | 6 | """ 7 | 8 | try: 9 | import rest_framework 10 | except ImportError as exc: # pragma: no cover 11 | raise ImportError("Package 'djangorestframework' is required to use this module.") from exc 12 | 13 | import rest_framework.fields 14 | 15 | from cl_sii.rut import Rut 16 | 17 | 18 | class RutField(rest_framework.fields.CharField): 19 | """ 20 | DRF field for RUT. 21 | 22 | Data types: 23 | * native/primitive/internal/deserialized: :class:`cl_sii.rut.Rut` 24 | * representation/serialized: str, same as for DRF field 25 | :class:`rest_framework.fields.CharField` 26 | 27 | It verifies only that the input is syntactically valid; it does NOT check 28 | that the value is within boundaries deemed acceptable by the SII. 29 | 30 | The field performs some input value cleaning when it is an str; 31 | for example ``' 1.111.111-k \t '`` is allowed and the resulting value 32 | is ``Rut('1111111-K')``. 33 | 34 | .. seealso:: 35 | :class:`.dj_model_fields.RutField` and :class:`.mm_fields.RutField` 36 | 37 | Implementation partially inspired in 38 | :class:`rest_framework.fields.UUIDField`. 39 | 40 | """ 41 | 42 | default_error_messages = { 43 | 'invalid': "'{value}' is not a syntactically valid RUT.", 44 | } 45 | 46 | def to_internal_value(self, data: object) -> Rut: 47 | """ 48 | Deserialize. 49 | 50 | > Restore a primitive datatype into its internal python representation. 51 | 52 | :raises rest_framework.exceptions.ValidationError: 53 | if the data can't be converted 54 | 55 | """ 56 | if isinstance(data, Rut): 57 | converted_data = data 58 | else: 59 | try: 60 | if isinstance(data, str): 61 | converted_data = Rut(data, validate_dv=False) 62 | else: 63 | self.fail('invalid', value=data) 64 | except (AttributeError, TypeError, ValueError): 65 | self.fail('invalid', value=data) 66 | 67 | return converted_data 68 | 69 | def to_representation(self, value: Rut) -> str: 70 | """ 71 | Serialize. 72 | 73 | > Convert the initial datatype into a primitive, serializable datatype. 74 | 75 | """ 76 | return value.canonical 77 | -------------------------------------------------------------------------------- /src/cl_sii/extras/dj_form_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | cl_sii "extras" / Django form fields. 3 | 4 | """ 5 | 6 | try: 7 | import django 8 | except ImportError as exc: # pragma: no cover 9 | raise ImportError("Package 'Django' is required to use this module.") from exc 10 | 11 | from typing import Any, Optional 12 | 13 | import django.core.exceptions 14 | import django.forms 15 | from django.utils.translation import gettext_lazy as _ 16 | 17 | from cl_sii.rut import Rut 18 | 19 | 20 | class RutField(django.forms.CharField): 21 | """ 22 | Django form field for RUT. 23 | 24 | * Python data type: :class:`cl_sii.rut.Rut` 25 | 26 | .. seealso:: 27 | :class:`.dj_model_fields.RutField` 28 | 29 | """ 30 | 31 | default_error_messages = { 32 | 'invalid': _('Enter a valid RUT.'), 33 | 'invalid_dv': _('RUT\'s "digito verificador" is incorrect.'), 34 | } 35 | 36 | def __init__(self, *, validate_dv: bool = False, **kwargs: Any) -> None: 37 | """ 38 | :param validate_dv: Boolean that specifies whether to validate that 39 | the RUT's "digito verificador" is correct. False by default. 40 | """ 41 | 42 | self.validate_dv = validate_dv 43 | super().__init__(strip=True, **kwargs) 44 | 45 | def to_python(self, value: Optional[object]) -> Optional[Rut]: 46 | """ 47 | Validate that the input can be converted to a Python object (:class:`Rut`). 48 | 49 | :raises django.core.exceptions.ValidationError: 50 | if the input can't be converted 51 | """ 52 | 53 | if value in self.empty_values: 54 | converted_value = None 55 | elif isinstance(value, Rut): 56 | converted_value = value 57 | else: 58 | try: 59 | converted_value = Rut(value) # type: ignore[arg-type] 60 | except (AttributeError, TypeError, ValueError): 61 | raise django.core.exceptions.ValidationError( 62 | self.error_messages['invalid'], 63 | code='invalid', 64 | ) 65 | 66 | if ( 67 | converted_value is not None 68 | and self.validate_dv 69 | and not converted_value.validate_dv(raise_exception=False) 70 | ): 71 | raise django.core.exceptions.ValidationError( 72 | self.error_messages['invalid_dv'], 73 | code='invalid_dv', 74 | ) 75 | 76 | return converted_value 77 | -------------------------------------------------------------------------------- /src/cl_sii/libs/io_utils.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import io 3 | from typing import IO 4 | 5 | 6 | # notes: 7 | # - For streams and modes see 'io.open()' 8 | # - Stream classes have a pretty strange 'typing'/ABC/inheritance/etc arrangement because, 9 | # among others, they are implemented in C. 10 | # - Use `IO[X]` for arguments and `TextIO`/`BinaryIO` for return types (says GVR). 11 | # https://github.com/python/typing/issues/518#issuecomment-350903120 12 | 13 | 14 | def with_mode_binary(stream: IO) -> bool: 15 | """ 16 | Return whether ``stream`` is a binary stream (i.e. reads bytes). 17 | """ 18 | result = False 19 | try: 20 | result = 'b' in stream.mode 21 | except AttributeError: 22 | if isinstance(stream, (io.RawIOBase, io.BufferedIOBase, io.BytesIO)): 23 | result = True 24 | 25 | return result 26 | 27 | 28 | def with_mode_text(stream: IO) -> bool: 29 | """ 30 | Return whether ``stream`` is a text stream (i.e. reads strings). 31 | """ 32 | result = False 33 | try: 34 | result = 't' in stream.mode 35 | except AttributeError: 36 | if isinstance(stream, (io.TextIOBase, io.TextIOWrapper, io.StringIO)): 37 | result = True 38 | 39 | return result 40 | 41 | 42 | def with_encoding_utf8(text_stream: IO[str]) -> bool: 43 | """ 44 | Return whether ``text_stream`` is a text stream with encoding set to UTF-8. 45 | 46 | :raises TypeError: if ``text_stream`` is not a text stream 47 | 48 | """ 49 | result = False 50 | 51 | if isinstance(text_stream, io.StringIO): 52 | # note: 'StringIO' saves (unicode) strings in memory and therefore doesn't have (or need) 53 | # an encoding, which is fine. 54 | # https://stackoverflow.com/questions/9368865/io-stringio-encoding-in-python3/9368909#9368909 55 | result = True 56 | else: 57 | try: 58 | text_stream_encoding: str = text_stream.encoding # type: ignore 59 | except AttributeError as exc: 60 | raise TypeError("Value is not a text stream.") from exc 61 | if text_stream_encoding is None: 62 | # e.g. the strange case of `tempfile.SpooledTemporaryFile(mode='rt', encoding='utf-8')` 63 | pass 64 | else: 65 | try: 66 | text_stream_encoding_norm = codecs.lookup(text_stream_encoding).name 67 | result = text_stream_encoding_norm == 'utf-8' 68 | except LookupError: 69 | pass 70 | 71 | return result 72 | -------------------------------------------------------------------------------- /src/scripts/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Example script 4 | ============== 5 | 6 | Does X and Y, and then Z. 7 | 8 | Example 9 | ------- 10 | 11 | For example, to do X, run:: 12 | 13 | ./scripts/example.py arg1 arg2 arg3 14 | 15 | """ 16 | import logging 17 | import os 18 | import sys 19 | from datetime import datetime 20 | from typing import Sequence 21 | 22 | 23 | try: 24 | import cl_sii # noqa: F401 25 | except ImportError: 26 | # If package 'cl-sii' is not installed, try appending the project repo directory to the 27 | # Python path, assuming thath we are in the project repo. If not, it will fail nonetheless. 28 | sys.path.append(os.path.dirname(os.path.abspath(__name__))) 29 | import cl_sii # noqa: F401 30 | 31 | 32 | logger = logging.getLogger(__name__) 33 | root_logger = logging.getLogger() 34 | 35 | 36 | ############################################################################### 37 | # logging config 38 | ############################################################################### 39 | 40 | _loggers = [logger, logging.getLogger('cl_sii')] 41 | for _logger in _loggers: 42 | _logger.addHandler(logging.StreamHandler()) 43 | _logger.setLevel(logging.INFO) 44 | 45 | root_logger.setLevel(logging.WARNING) 46 | 47 | 48 | ############################################################################### 49 | # script 50 | ############################################################################### 51 | 52 | 53 | def main(args: Sequence[str]) -> None: 54 | start_ts = datetime.now() 55 | 56 | logger.debug("Example script. Args: %s", args) 57 | 58 | try: 59 | print("Action: do something") 60 | except FileNotFoundError: 61 | logger.exception("Process aborted: a file could not be opened.", exc_info=True) 62 | except KeyboardInterrupt: 63 | logger.error("Process interrupted by user.") 64 | except Exception: 65 | logger.exception("Process aborted.") 66 | finally: 67 | try: 68 | print("Action: clean up resources and connections") 69 | logger.info("Cleaned up resources and connections.") 70 | except Exception: 71 | logger.exception("Failed to clean up resources and connections.") 72 | 73 | finish_ts = datetime.now() 74 | duration = finish_ts - start_ts 75 | 76 | logger.info(f"start: {start_ts.isoformat()}") 77 | logger.info(f"finish: {finish_ts.isoformat()}") 78 | logger.info(f"duration: {duration!s}") 79 | 80 | 81 | if __name__ == '__main__': 82 | main(sys.argv[1:]) 83 | -------------------------------------------------------------------------------- /src/cl_sii/rut/crypto_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional 3 | 4 | import cryptography 5 | import cryptography.hazmat.backends.openssl.backend as crypto_x509_backend 6 | import cryptography.hazmat.primitives.serialization.pkcs12 7 | import cryptography.x509 8 | 9 | from . import Rut, constants 10 | 11 | 12 | def get_subject_rut_from_certificate_pfx(pfx_file_bytes: bytes, password: Optional[str]) -> Rut: 13 | """ 14 | Return the Chilean RUT stored in a digital certificate. 15 | 16 | Original source URL: https://github.com/fyntex/fd-cl-data/blob/cfd5a716fb9b2cbd8a03fca1bacfd1b844b1337f/fd_cl_data/apps/sii_auth/models/sii_auth_credential.py#L701-L745 # noqa: E501 17 | 18 | :param pfx_file_bytes: Digital certificate in PKCS12 format 19 | :param password: (Optional) The password to use to decrypt the PKCS12 file 20 | """ 21 | ( 22 | private_key, 23 | x509_cert, 24 | additional_certs, 25 | ) = cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates( 26 | data=pfx_file_bytes, 27 | password=password.encode() if password is not None else None, 28 | backend=crypto_x509_backend, 29 | ) 30 | # https://cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/#cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates # noqa: E501 31 | 32 | assert x509_cert is not None 33 | 34 | subject_alt_name_ext = x509_cert.extensions.get_extension_for_class( 35 | cryptography.x509.extensions.SubjectAlternativeName, 36 | ) 37 | 38 | # Search for the RUT in the certificate. 39 | try: 40 | results = [ 41 | x.value 42 | for x in subject_alt_name_ext.value._general_names 43 | if hasattr(x, 'type_id') and x.type_id == constants.SII_CERT_TITULAR_RUT_OID 44 | ] 45 | except AttributeError as exc: 46 | raise Exception(f'Malformed certificate extension: {subject_alt_name_ext.oid}') from exc 47 | 48 | if not results: 49 | raise Exception('Certificate has no RUT information') 50 | elif len(results) > 1: 51 | raise Exception(f'len(results) == {len(results)}') 52 | 53 | subject_rut_raw: bytes = results[0] 54 | subject_rut_str = subject_rut_raw.decode('utf-8') 55 | 56 | # Regex to extract Chilean RUT formatted string 57 | rut_match = re.search(r'\b\d{1,8}-[0-9Kk]\b', subject_rut_str) 58 | 59 | if not rut_match: 60 | raise Exception('RUT format not found in certificate') 61 | 62 | subject_rut = rut_match.group(0) 63 | 64 | return Rut(subject_rut) 65 | -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/LceCoCertif_v10.xsd: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Comprobante de Certificacion 14 | 15 | 16 | 17 | 18 | 19 | Documento de Comprobante de Certificacion 20 | 21 | 22 | 23 | 24 | 25 | RUT Contribuyente de los LCE 26 | 27 | 28 | 29 | 30 | Fecha de Emision del Comprobante de Certificacion (AAAA-MM-DD) 31 | 32 | 33 | 34 | 35 | 36 | RUT autorizado por el Distribuidor a firmar este documento. 37 | 38 | 39 | 40 | 41 | Fecha y Hora de la Firma 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Firma Digital sobre Documento 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | DTE con Imagen y Recibos 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Representacion XML del DTE Cedido 20 | 21 | 22 | 23 | 24 | Representacion PDF del DTE Cedido 25 | 26 | 27 | 28 | 29 | Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor 30 | 31 | 32 | 33 | 34 | Representacion PDF del los Acuse de Recibo 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Fecha y Hora en que se Firmo Digitalmente el Documento Cedido AAAA-MM-DDTHH:MI:SS 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Firma Digital sobre Documento 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cl-sii Python lib 2 | 3 | [![PyPI Package Version](https://img.shields.io/pypi/v/cl-sii)](https://pypi.org/project/cl-sii/) 4 | [![Python Versions](https://img.shields.io/pypi/pyversions/cl-sii)](https://pypi.org/project/cl-sii/) 5 | [![License](https://img.shields.io/pypi/l/cl-sii)](https://pypi.org/project/cl-sii/) 6 | 7 | Python library for Servicio de Impuestos Internos (SII) of Chile. 8 | 9 | ## Documentation 10 | 11 | The full documentation is at . 12 | 13 | ## Dashboard 14 | 15 | ### Development 16 | 17 | | VCS Branch | Deployment Environment | VCS Repository | CI/CD Status | 18 | | ---------- | ---------------------- | -------------- | ------------ | 19 | | `develop` | Staging | [GitHub](https://github.com/fyntex/lib-cl-sii-python/tree/develop) | [![GitHub Actions](https://github.com/fyntex/lib-cl-sii-python/actions/workflows/ci-cd.yaml/badge.svg?branch=develop)](https://github.com/fyntex/lib-cl-sii-python/actions/workflows/ci-cd.yaml?query=branch:develop) | 20 | | `master` | Production | [GitHub](https://github.com/fyntex/lib-cl-sii-python/tree/master) | [![GitHub Actions](https://github.com/fyntex/lib-cl-sii-python/actions/workflows/ci-cd.yaml/badge.svg?branch=master)](https://github.com/fyntex/lib-cl-sii-python/actions/workflows/ci-cd.yaml?query=branch:master) | 21 | 22 | | Code Coverage | Code Climate | Documentation | Project Analysis | 23 | | ------------- | ------------ | ------------- | ---------------- | 24 | | [![Codecov](https://codecov.io/gh/cordada/lib-cl-sii-python/graph/badge.svg?token=VdwPUEUzzQ)](https://codecov.io/gh/cordada/lib-cl-sii-python) | [![Maintainability](https://api.codeclimate.com/v1/badges/c4e8a9b023310ff8c276/maintainability)](https://codeclimate.com/github/fyntex/lib-cl-sii-python/maintainability) | [![Read the Docs](https://readthedocs.org/projects/lib-cl-sii-python/badge/)](https://readthedocs.org/projects/lib-cl-sii-python/) | [Open Source Insights](https://deps.dev/pypi/cl-sii) | 25 | 26 | ### Hosting 27 | 28 | | Deployment Environment | Python Package Registry | 29 | | ---------------------- | ----------------------- | 30 | | Production | [PyPI](https://pypi.org/project/cl-sii/) | 31 | 32 | ## Supported Python versions 33 | 34 | Only Python 3.9, 3.10, 3.11, 3.12, and 3.13. Python 3.8 and below will not work because we use some 35 | features introduced in Python 3.9. 36 | 37 | ## Quickstart 38 | 39 | Install package:: 40 | 41 | ```sh 42 | pip install cl-sii 43 | ``` 44 | 45 | And TODO 46 | 47 | ## Features 48 | 49 | - TODO 50 | 51 | ### Tests 52 | 53 | Requirements:: 54 | 55 | ```sh 56 | make install-dev 57 | ``` 58 | 59 | Run test suite for all supported Python versions and run tools for 60 | code style analysis, static type check, etc:: 61 | 62 | ```sh 63 | make test-all 64 | make lint 65 | ``` 66 | 67 | Check code coverage of tests:: 68 | 69 | ```sh 70 | make test-coverage 71 | make test-coverage-report-console 72 | ``` 73 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 5 | 170 6 | 2019-04-01 7 | 1 8 | 1 9 | 2 10 | 11 | 12 | 76354771-K 13 | INGENIERIA ENACON SPA 14 | Ingenieria y Construccion 15 | ENACONLTDA@GMAIL.COM 16 | 421000 17 | 078525666 18 | MERCED 753 16 ARBOLEDA DE QUIILOTA 19 | QUILLOTA 20 | QUILLOTA 21 | 22 | 23 | 96790240-3 24 | MINERA LOS PELAMBRES 25 | EXTRACCION Y PROCESAMIENTO DE COBRE 26 | Felipe Barria 27 | Av. Apoquindo 4001 1802 28 | LAS CONDES 29 | SANTIAGO 30 | 31 | 32 | 2517900 33 | 19.00 34 | 478401 35 | 2996301 36 | 37 | 38 | 39 | 1 40 | Tableros electricos 3 tom 41 | as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- 42 | 2.00 43 | Unid 44 | 1258950.00 45 | 2517900 46 | 47 | 48 | 1 49 | 801 50 | 4510083633 51 | 2019-03-22 52 | 53 |
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
54 | 2019-04-01T01:36:40 55 | 56 |
-------------------------------------------------------------------------------- /src/cl_sii/rut/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | RUT-related constants. 3 | 4 | Source: XML type 'RUTType' in official schema 'SiiTypes_v10.xsd'. 5 | https://github.com/fyntex/lib-cl-sii-python/blob/f57a326/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd#L127-L136 6 | 7 | """ 8 | 9 | import re 10 | from typing import Pattern 11 | 12 | import cryptography.x509 13 | 14 | 15 | RUT_CANONICAL_STRICT_REGEX = re.compile(r'^(?P\d{1,8})-(?P[\dK])$') 16 | """RUT (strict) regex for canonical format.""" 17 | RUT_CANONICAL_MAX_LENGTH = 10 18 | """RUT max length for canonical format.""" 19 | RUT_CANONICAL_MIN_LENGTH = 3 20 | """RUT min length for canonical format.""" 21 | RUT_DIGITS_MAX_VALUE = 99999999 22 | """RUT digits max value.""" 23 | RUT_DIGITS_MIN_VALUE = 1 24 | """RUT digits min value.""" 25 | 26 | RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX: Pattern[str] = re.compile("^(\\d{1,8})-([\\dK])$") 27 | """ 28 | RUT (strict) JSON Schema regex for canonical format. 29 | 30 | This regex is compatible with JSON Schema and OpenAPI, which use the regular expression syntax from 31 | JavaScript (ECMA 262), which does not support Python’s named groups. 32 | 33 | .. tip:: If you need the regex as a string, for example to use it in a JSON Schema or 34 | OpenAPI schema, use ``RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX.pattern``. 35 | """ 36 | 37 | SII_CERT_TITULAR_RUT_OID = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.4.1.8321.1") 38 | """OID of the RUT of the certificate holder""" 39 | # - Organismo: MINISTERIO DE ECONOMÍA / SUBSECRETARIA DE ECONOMIA 40 | # - Decreto 181 (Julio-Agosto 2002) 41 | # "APRUEBA REGLAMENTO DE LA LEY 19.799 SOBRE DOCUMENTOS ELECTRONICOS, FIRMA ELECTRONICA 42 | # Y LA CERTIFICACION DE DICHA FIRMA" 43 | # - ref: https://www.leychile.cl/Consulta/m/norma_plana?org=&idNorma=201668 44 | # dice: 45 | # > RUT del titular del certificado : 1.3.6.1.4.1.8321.1 46 | # > RUT de la certificadora emisora : 1.3.6.1.4.1.8321.2 47 | # 48 | # - ref: http://acepta.newtenberg.com/1919/articles-82538_recurso_3.pdf 49 | # dice: 50 | # > OtherName: Para certificados de identidad de individuos, aquí se registra el RUT, en 51 | # > la siguiente estructura: 52 | # Type-id = 1.3.6.1.4.1.8321.1 53 | # Value ='xx.xxx.xx-v' 54 | # > El campo Value es un IA5String con el RUT del individuo titular del certificado. 55 | 56 | PERSONA_JURIDICA_MIN_RUT_DIGITS: int = 50000000 57 | """ 58 | Lowest RUT digits for “personas jurídicas”. 59 | """ 60 | # Why must “personas jurídicas” have RUT ≥ 50000000-7? 61 | # 62 | # > ¿Qué es una Persona Jurídica? 63 | # > 64 | # > […] persona ficticia, capaz de ejercer derechos y contraer obligaciones civiles, y de ser 65 | # > representada judicial y extrajudicialmente. Además de esto, poseen Rut sobre 50 millones. 66 | # 67 | # Source: 68 | # [BancoEstado Microempresas → Información general sobre personas jurídicas](https://www.bancoestado.cl/content/bancoestado-public/cl/es/home/home-microempresa/servicios/informacion-general-sobre-personas-juridicas---bancoestado-micro.html#/) # noqa: E501 69 | # (retrieved on 2025-01-28) 70 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 6 | 170 7 | 2019-04-01 8 | 1 9 | 1 10 | 2 11 | 12 | 13 | 76354771-K 14 | INGENIERIA ENACON SPA 15 | Ingenieria y Construccion 16 | ENACONLTDA@GMAIL.COM 17 | 421000 18 | 078525666 19 | MERCED 753 16 ARBOLEDA DE QUIILOTA 20 | QUILLOTA 21 | QUILLOTA 22 | 23 | 24 | 96790240-3 25 | MINERA LOS PELAMBRES 26 | EXTRACCION Y PROCESAMIENTO DE COBRE 27 | Felipe Barria 28 | Av. Apoquindo 4001 1802 29 | LAS CONDES 30 | SANTIAGO 31 | 32 | 33 | 2517900 34 | 19.00 35 | 478401 36 | 2996301 37 | 38 | 39 | 40 | 1 41 | Tableros electricos 3 tom 42 | as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- 43 | 2.00 44 | Unid 45 | 1258950.00 46 | 2517900 47 | 48 | 49 | 1 50 | 801 51 | 4510083633 52 | 2019-03-22 53 | 54 |
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
55 | 2019-04-01T01:36:40 56 | 57 |
-------------------------------------------------------------------------------- /src/tests/test_data/crypto/wildcard-google-com-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU 3 | MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw 4 | IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz 5 | NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh 6 | bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds 7 | ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 8 | AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ 9 | gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF 10 | BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu 11 | Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs 12 | b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv 13 | gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz 14 | LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO 15 | Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC 16 | DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u 17 | Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j 18 | b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou 19 | Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs 20 | ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu 21 | Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n 22 | b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n 23 | c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp 24 | Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq 25 | LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl 26 | ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt 27 | Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb 28 | ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu 29 | Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0 30 | aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t 31 | ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK 32 | d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0 33 | aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow 34 | LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp 35 | BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O 36 | BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw 37 | FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD 38 | MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n 39 | L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y 40 | ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ 41 | IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb 42 | +DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl 43 | g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP 44 | MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA 45 | ow== 46 | -----END CERTIFICATE----- 47 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 8 | 170 9 | 2019-04-01 10 | 1 11 | 1 12 | 2 13 | 14 | 15 | 76354771-K 16 | INGENIERIA ENACON SPA 17 | Ingenieria y Construccion 18 | ENACONLTDA@GMAIL.COM 19 | 421000 20 | 078525666 21 | MERCED 753 16 ARBOLEDA DE QUIILOTA 22 | QUILLOTA 23 | QUILLOTA 24 | 25 | 26 | 96790240-3 27 | MINERA LOS PELAMBRES 28 | EXTRACCION Y PROCESAMIENTO DE COBRE 29 | Felipe Barria 30 | Av. Apoquindo 4001 1802 31 | LAS CONDES 32 | SANTIAGO 33 | 34 | 35 | 2517900 36 | 19.00 37 | 478401 38 | 2996301 39 | 40 | 41 | 42 | 1 43 | Tableros electricos 3 tom 44 | as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- 45 | 2.00 46 | Unid 47 | 1258950.00 48 | 2517900 49 | 50 | 51 | 1 52 | 801 53 | 4510083633 54 | 2019-03-22 55 | 56 |
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
57 | 2019-04-01T01:36:40 58 | 59 |
60 |
61 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n-signature_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | ZeVB66oxAZfjbS8ls+4OkLu838g=dHnmsO61gKLtSMbHtOooDjBbcjNMsQjkE/sAehhTlSiXw830v/We+nXYhpFpCsA0AaqBlMC6RQN0bOvxsGNJI477apkGOGgj87MOs02hiI5pmmtVsn+ohMv+Vc/ISEN3qVIfyWF02EFXVbvnifhq1dSWDp78RKH/9Tqaq4ww+N0=qbtbVOYnBr9R4L+O18R/1RdxiX+hxBINamgJKqUczj3xfbL/N1KNt9Mj+Q7OW2xfCmZ9L/tWmIuLteqX1cSloH6CPI2xwQVG5B5pU/GQl0/CC3vshjWiY9sWs/qAgXmRQiZCCpkOIHk25Pc9pdN7BfyocWDSIIW1nLP9geQsEy8=AQABMIIGNzCCBR+gAwIBAgIKYRvSawAAAAhSHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wxHTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UEChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQDEydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEWGHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA3MDcxNTAxMTVaFw0xOTA3MDcxNTAxMTVaMIG6MQswCQYDVQQGEwJDTDEjMCEGA1UECBMaTUVUUk9QT0xJVEFOQSBERSBTQU5USUFHTyAxETAPBgNVBAcTCFNhbnRpYWdvMRgwFgYDVQQKEw9TVCBDQVBJVEFMIFMuQS4xEjAQBgNVBAsTCUZBQ1RPUklORzEbMBkGA1UEAxMSQU5EUkVTICBQUkFUUyBWSUFMMSgwJgYJKoZIhvcNAQkBFhlwZ2FsdmV6bXVub3pAc3RjYXBpdGFsLmNsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpu1tU5icGv1Hgv47XxH/VF3GJf6HEEg1qaAkqpRzOPfF9sv83Uo230yP5Ds5bbF8KZn0v+1aYi4u16pfVxKWgfoI8jbHBBUbkHmlT8ZCXT8ILe+yGNaJj2xaz+oCBeZFCJkIKmQ4geTbk9z2l03sF/KhxYNIghbWcs/2B5CwTLwIDAQABo4ICpzCCAqMwPQYJKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIgtyDL4WTjGaF1Z0XguLcJ4Hv7DxhhNWJYoWMtRECAWQCAQQwHQYDVR0OBBYEFKQ6r6OId16o1Wd37wqvn1k9IFPyMAsGA1UdDwQEAwIE8DAfBgNVHSMEGDAWgBR44T6f0hKzejyNzTAOU7NDKQezVTA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY3JsLmUtY2VydGNoaWxlLmNsL2VjZXJ0Y2hpbGVjYUZFUy5jcmwwOgYIKwYBBQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5lY2VydGNoaWxlLmNsL29jc3AwIwYDVR0RBBwwGqAYBggrBgEEAcEBAaAMFgoxNjM2MDM3OS05MCMGA1UdEgQcMBqgGAYIKwYBBAHBAQKgDBYKOTY5MjgxODAtNTCCAU0GA1UdIASCAUQwggFAMIIBPAYIKwYBBAHDUgUwggEuMC0GCCsGAQUFBwIBFiFodHRwOi8vd3d3LmUtY2VydGNoaWxlLmNsL0NQUy5odG0wgfwGCCsGAQUFBwICMIHvHoHsAEMAZQByAHQAaQBmAGkAYwBhAGQAbwAgAEYAaQByAG0AYQAgAFMAaQBtAHAAbABlAC4AIABIAGEAIABzAGkAZABvACAAdgBhAGwAaQBkAGEAZABvACAAZQBuACAAZgBvAHIAbQBhACAAcAByAGUAcwBlAG4AYwBpAGEAbAAsACAAcQB1AGUAZABhAG4AZABvACAAaABhAGIAaQBsAGkAdABhAGQAbwAgAGUAbAAgAEMAZQByAHQAaQBmAGkAYwBhAGQAbwAgAHAAYQByAGEAIAB1AHMAbwAgAHQAcgBpAGIAdQB0AGEAcgBpAG8wDQYJKoZIhvcNAQEFBQADggEBAGGMlOafoMAlJxsg3e+vvALtwz+AJ/YXR5X7mRWlA3glkGu75rMTj5JSTuc7PbxkxTccR31vMxxhCISMLKtwRpYrifdTc5RzHCFgZUlgR+PLW+U9Wcwxnp1dX3IjG0XgdG2FY0OvmzT38gBe8lhtDfMPQa//1FyyVOHAsz2l9SpTfEtDVX0AG6RV/hJl7VJcZl6mtsGiPjCWGRvjAXA9Y8+FzTvHRU8JNcGrBMnH7CmnVTOQMbgm4ZlZTvV9EdzX/7eIGD5bIvvnXRQM/S7oRnHl+81nEgTSYDDgwU3E625am3LMrN09786qEx5VhoU8pL57MWkof28uFPdpvzjgEG4= -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for Release 2 | 3 | name: Release 4 | 5 | on: 6 | workflow_call: 7 | inputs: 8 | create_git_tag_and_github_release: 9 | type: boolean 10 | required: true 11 | description: Create Git tag and GitHub release. 12 | outputs: 13 | artifacts_path: 14 | value: ${{ jobs.release.outputs.artifacts_path }} 15 | 16 | permissions: 17 | contents: read 18 | 19 | env: 20 | PYTHON_VIRTUALENV_ACTIVATE: venv/bin/activate 21 | 22 | jobs: 23 | release: 24 | name: Release 25 | runs-on: ubuntu-22.04 26 | 27 | permissions: 28 | contents: write 29 | 30 | env: 31 | ARTIFACTS_PATH: dist 32 | 33 | outputs: 34 | artifacts_path: ${{ env.ARTIFACTS_PATH }} 35 | 36 | steps: 37 | - name: Check Out VCS Repository 38 | uses: actions/checkout@v6.0.0 39 | 40 | - name: Set Up Python 41 | id: set_up_python 42 | uses: actions/setup-python@v6.1.0 43 | with: 44 | python-version: "3.13" 45 | 46 | - name: Create Python Virtual Environment 47 | run: make python-virtualenv PYTHON_VIRTUALENV_DIR="venv" 48 | 49 | - name: Restoring/Saving Cache 50 | uses: actions/cache@v4.3.0 51 | with: 52 | path: "venv" 53 | key: py-v1-deps-${{ runner.os }}-${{ steps.set_up_python.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'requirements.txt', 'requirements-dev.txt', 'Makefile', 'make/**.mk') }} 54 | 55 | - name: Install Dependencies 56 | run: | 57 | source "$PYTHON_VIRTUALENV_ACTIVATE" 58 | make install-deps-dev 59 | 60 | - name: Build 61 | run: | 62 | source "$PYTHON_VIRTUALENV_ACTIVATE" 63 | make clean-build build 64 | 65 | - name: Build for Distribution 66 | run: | 67 | source "$PYTHON_VIRTUALENV_ACTIVATE" 68 | make dist 69 | 70 | - name: Store Artifacts 71 | uses: actions/upload-artifact@v5.0.0 72 | with: 73 | name: release 74 | path: ${{ env.ARTIFACTS_PATH }}/ 75 | if-no-files-found: error 76 | retention-days: 1 77 | 78 | - name: Set Release Configuration 79 | id: set_release_config 80 | run: | 81 | source "$PYTHON_VIRTUALENV_ACTIVATE" 82 | 83 | library_version=$(python3 ./setup.py --version) 84 | 85 | echo "library_version=${library_version:?}" >> "$GITHUB_OUTPUT" 86 | 87 | - name: Create Git Tag and GitHub Release 88 | if: ${{ inputs.create_git_tag_and_github_release }} 89 | run: | 90 | gh release create \ 91 | "${VCS_TAG_NAME:?}" \ 92 | --target "${TARGET_VCS_REF:?}" \ 93 | --generate-notes \ 94 | ${ASSET_FILES:?} 95 | 96 | echo "library_vcs_tag_name=${VCS_TAG_NAME:?}" >> "$GITHUB_OUTPUT" 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | VCS_TAG_NAME: v${{ steps.set_release_config.outputs.library_version }} 100 | TARGET_VCS_REF: ${{ github.sha }} 101 | ASSET_FILES: ${{ env.ARTIFACTS_PATH }}/* 102 | -------------------------------------------------------------------------------- /src/cl_sii/rtc/constants.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import FrozenSet 3 | 4 | from cl_sii.dte.constants import DTE_MONTO_TOTAL_FIELD_MAX_VALUE, TipoDte 5 | 6 | 7 | # The collection of "tipo DTE" for which it is possible to "ceder" a "DTE". 8 | # They are defined in a document and also an XML schema. 9 | # - Document "Formato Archivo Electrónico de Cesión (AEC)" 10 | # (http://www.sii.cl/factura_electronica/cesion.pdf) are: 11 | # > Sólo códigos 33, 34, 46 y 43 12 | # - XML element 'CesionDefType/DocumentoCesion/IdDTE/TipoDTE' 13 | # - description: "Tipo de DTE" 14 | # - XML type: 'SiiDte:DTEFacturasType' 15 | # - source: 16 | # https://github.com/fyntex/lib-cl-sii-python/blob/7e1c4b52/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd#L38-L42 17 | # - XML type 'SiiDte:DTEFacturasType' in official schema 'SiiTypes_v10.xsd' 18 | # - source: 19 | # https://github.com/fyntex/lib-cl-sii-python/blob/7e1c4b52/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd#L100-L126 20 | TIPO_DTE_CEDIBLES: FrozenSet[TipoDte] = frozenset( 21 | { 22 | TipoDte.FACTURA_ELECTRONICA, 23 | TipoDte.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA, 24 | TipoDte.FACTURA_COMPRA_ELECTRONICA, 25 | TipoDte.LIQUIDACION_FACTURA_ELECTRONICA, 26 | } 27 | ) 28 | 29 | 30 | ############################################################################### 31 | # Cesion Fields / "Monto Cedido" 32 | ############################################################################### 33 | 34 | # Amount of the "cesión". 35 | # 36 | # Ref: 37 | # - https://github.com/fyntex/lib-cl-sii-api-python/blob/v0.4.4/cl_sii_api/rtc/data_models.py#L231 38 | # - Document "Formato Archivo Electrónico de Cesión 2013-02-11" (retrieved on 2019-08-12) 39 | # (https://www.sii.cl/factura_electronica/cesion.pdf) 40 | CESION_MONTO_CEDIDO_FIELD_MIN_VALUE: int = 0 41 | CESION_MONTO_CEDIDO_FIELD_MAX_VALUE: int = DTE_MONTO_TOTAL_FIELD_MAX_VALUE 42 | 43 | 44 | ############################################################################### 45 | # Cesion Fields / "Secuencia" 46 | ############################################################################### 47 | 48 | # Sequence number of the "cesión" 49 | # 50 | # > Campo: Número de Cesión 51 | # > Descripción: Secuencia de la cesión 52 | # > Tipo: NUM 53 | # > Validación: 1 hasta 40 54 | # 55 | # Source: 56 | # Document "Formato Archivo Electrónico de Cesión 2013-02-11" (retrieved on 2019-08-12) 57 | # (https://www.sii.cl/factura_electronica/cesion.pdf) 58 | CESION_SEQUENCE_NUMBER_MIN_VALUE: int = 1 59 | CESION_SEQUENCE_NUMBER_MAX_VALUE: int = 40 60 | 61 | 62 | ############################################################################### 63 | # Other 64 | ############################################################################### 65 | 66 | 67 | @enum.unique 68 | class RolContribuyenteEnCesion(enum.Enum): 69 | """ 70 | "Rol" of "contribuyente" in a "cesion". 71 | """ 72 | 73 | CEDENTE = 'CEDENTE' 74 | """Cesiones en las que el contribuyente ha sido cedente i.e. ha cedido""" 75 | 76 | CESIONARIO = 'CESIONARIO' 77 | """Cesiones en las que el contribuyente ha sido cesionario i.e. le han cedido""" 78 | 79 | DEUDOR = 'DEUDOR' 80 | """Cesiones de DTEs en que el contribuyente es el deudor.""" 81 | -------------------------------------------------------------------------------- /.github/workflows/task-release-and-deploy.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for 'Release and Deploy' Task 2 | 3 | name: "Task: Release and Deploy" 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - closed 9 | branches: # Base reference 10 | - develop 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | deploy: 16 | name: Deploy 17 | if: ${{ github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/') }} 18 | runs-on: ubuntu-22.04 19 | 20 | permissions: 21 | contents: write 22 | pull-requests: write 23 | 24 | env: 25 | CREATE_RELEASE_VCS_REVISION_ID: ${{ github.sha }} # Merge commit in base branch. 26 | PREPARE_RELEASE_GITHUB_PULL_REQUEST_HTML_URL: ${{ github.event.pull_request.html_url }} 27 | PREPARE_RELEASE_GITHUB_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} 28 | PREPARE_RELEASE_GITHUB_VCS_REF: ${{ github.event.pull_request.head.ref }} 29 | RELEASE_ASSIGNEE: ${{ github.event.pull_request.assignee.login }} 30 | RELEASE_VCS_REF: refs/heads/master 31 | 32 | steps: 33 | - name: Check Out VCS Repository 34 | uses: actions/checkout@v6.0.0 35 | with: 36 | ref: ${{ env.CREATE_RELEASE_VCS_REVISION_ID }} 37 | 38 | - name: Prepare Git 39 | run: | 40 | echo 'Adding Git aliases…' 41 | git config alias.publish \ 42 | 'push --set-upstream origin HEAD' 43 | 44 | - name: Prepare Pull Request for Deployment 45 | run: | 46 | create_release_vcs_branch_name="${PREPARE_RELEASE_GITHUB_VCS_REF:?}" 47 | create_release_vcs_branch_name="${create_release_vcs_branch_name/release/deploy}" 48 | echo "Creating release creation VCS branch '$create_release_vcs_branch_name'…" 49 | git checkout -b "${create_release_vcs_branch_name:?}" -- 50 | git publish --verbose 51 | 52 | create_release_vcs_ref="refs/heads/${create_release_vcs_branch_name:?}" 53 | echo "CREATE_RELEASE_VCS_REF=${create_release_vcs_ref:?}" >> "$GITHUB_ENV" 54 | 55 | create_release_github_pull_request_title="${PREPARE_RELEASE_GITHUB_PULL_REQUEST_TITLE:?}" 56 | create_release_github_pull_request_title="deploy ${create_release_github_pull_request_title,,}" 57 | create_release_github_pull_request_title="${create_release_github_pull_request_title@u}" 58 | echo "CREATE_RELEASE_GITHUB_PULL_REQUEST_TITLE=${create_release_github_pull_request_title:?}" >> "$GITHUB_ENV" 59 | 60 | create_release_github_pull_request_description="Ref: ${PREPARE_RELEASE_GITHUB_PULL_REQUEST_HTML_URL:?}" 61 | echo "CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION=${create_release_github_pull_request_description:?}" >> "$GITHUB_ENV" 62 | 63 | - name: Create GitHub Pull Request for Deployment 64 | run: | 65 | gh pr create \ 66 | --base "$RELEASE_VCS_REF" \ 67 | --head "$CREATE_RELEASE_VCS_REF" \ 68 | --draft \ 69 | --title "$CREATE_RELEASE_GITHUB_PULL_REQUEST_TITLE" \ 70 | --body "$CREATE_RELEASE_GITHUB_PULL_REQUEST_DESCRIPTION" \ 71 | --assignee "$RELEASE_ASSIGNEE" \ 72 | --label 'task' \ 73 | --label 'kind: deploy' 74 | env: 75 | GH_TOKEN: ${{ github.token }} 76 | -------------------------------------------------------------------------------- /src/cl_sii/extras/dj_filters.py: -------------------------------------------------------------------------------- 1 | """ 2 | cl_sii "extras" / Django-Filter. 3 | 4 | (for Django views and DRF views) 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | 10 | try: 11 | import django_filters 12 | except ImportError as exc: # pragma: no cover 13 | raise ImportError("Package 'django-filter' is required to use this module.") from exc 14 | 15 | from collections.abc import Sequence 16 | from copy import deepcopy 17 | from typing import Any, ClassVar, Mapping, Type 18 | 19 | import django.db.models 20 | import django.forms 21 | 22 | import cl_sii.extras.dj_form_fields 23 | import cl_sii.extras.dj_model_fields 24 | 25 | 26 | FILTER_FOR_DBFIELD_DEFAULTS: Mapping[Type[django.db.models.Field], Mapping[str, object]] = {} 27 | 28 | 29 | class RutFilter(django_filters.filters.CharFilter): 30 | """ 31 | Matches on a RUT. 32 | 33 | Used with :class:`cl_sii.extras.dj_form_fields.RutField` by default. 34 | 35 | .. seealso:: 36 | - https://django-filter.readthedocs.io/en/stable/ref/filters.html 37 | - https://github.com/carltongibson/django-filter/blob/24.2/docs/ref/filters.txt 38 | """ 39 | 40 | field_class: Type[django.forms.Field] 41 | field_class = cl_sii.extras.dj_form_fields.RutField 42 | 43 | field_class_for_substrings: Type[django.forms.Field] 44 | field_class_for_substrings = django_filters.filters.CharFilter.field_class 45 | 46 | lookup_expressions_for_substrings: Sequence[str] = [ 47 | 'contains', 48 | 'icontains', 49 | 'startswith', 50 | 'istartswith', 51 | 'endswith', 52 | 'iendswith', 53 | ] 54 | 55 | def __init__( 56 | self, 57 | field_name: Any = None, 58 | lookup_expr: Any = None, 59 | *args: Any, 60 | **kwargs: Any, 61 | ) -> None: 62 | if lookup_expr in self.lookup_expressions_for_substrings: 63 | # Lookups that can be used to search for substrings will not always 64 | # work with the default field class because some substrings cannot 65 | # be converted to instances of class `Rut`. For example, 66 | # `…__contains="803"` fails because `Rut("803")` raises a `ValueError`. 67 | self.field_class = self.field_class_for_substrings 68 | 69 | super().__init__(field_name, lookup_expr, *args, **kwargs) 70 | 71 | 72 | FILTER_FOR_DBFIELD_DEFAULTS = { 73 | **FILTER_FOR_DBFIELD_DEFAULTS, 74 | cl_sii.extras.dj_model_fields.RutField: {'filter_class': RutFilter}, 75 | } 76 | 77 | 78 | class SiiFilterSet(django_filters.filterset.FilterSet): 79 | """ 80 | Custom filterset with extra database field mappings. 81 | 82 | This class serves as a base class for filtersets that additionally need to 83 | support filtering one of the following database fields: 84 | - :class:`cl_sii.extras.dj_model_fields.RutField` 85 | 86 | .. seealso:: 87 | - https://django-filter.readthedocs.io/en/main/ref/filterset.html 88 | - https://github.com/carltongibson/django-filter/blob/24.2/docs/ref/filterset.txt 89 | """ 90 | 91 | FILTER_DEFAULTS: ClassVar[Mapping[Type[django.db.models.Field], Mapping[str, object]]] 92 | FILTER_DEFAULTS = { 93 | **deepcopy(django_filters.FilterSet.FILTER_DEFAULTS), 94 | **FILTER_FOR_DBFIELD_DEFAULTS, 95 | } 96 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Python Project Configuration 2 | # 3 | # Documentation: 4 | # - https://packaging.python.org/en/latest/specifications/pyproject-toml/ 5 | # (https://github.com/pypa/packaging.python.org/blob/caa20073/source/specifications/pyproject-toml.rst) 6 | # - https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/ 7 | # (https://github.com/pypa/pip/blob/24.2/docs/html/reference/build-system/pyproject-toml.md) 8 | # - https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html 9 | # (https://github.com/pypa/setuptools/blob/v70.3.0/docs/userguide/pyproject_config.rst) 10 | 11 | [build-system] 12 | requires = [ 13 | "setuptools==80.9.0", 14 | ] 15 | build-backend = "setuptools.build_meta" 16 | 17 | [project] 18 | name = "cl-sii" 19 | dependencies = [ 20 | "cryptography>=43.0.0", 21 | "defusedxml>=0.6.0,<1", 22 | "jsonschema>=3.1.1", 23 | "lxml>=5.2.1,<7", 24 | "marshmallow>=3,<5", 25 | "pydantic>=2.10.0,!=1.7.*,!=1.8.*,!=1.9.*", 26 | "pyOpenSSL>=24.0.0", 27 | "pytz>=2019.3", 28 | "signxml>=4.0.0", 29 | "typing-extensions>=4.0.1", 30 | ] 31 | requires-python = ">=3.9, <3.14" 32 | authors = [ 33 | {name = "Fyntex TI SpA", email = "no-reply@fyntex.ai"}, 34 | ] 35 | description = "Python library for Servicio de Impuestos Internos (SII) of Chile." 36 | readme = "README.md" 37 | license = "MIT" 38 | license-files = ["LICENSE"] 39 | classifiers = [ 40 | # See https://pypi.org/classifiers/ 41 | "Development Status :: 3 - Alpha", 42 | "Intended Audience :: Developers", 43 | "Natural Language :: English", 44 | "Programming Language :: Python :: 3", 45 | "Programming Language :: Python :: 3.9", 46 | "Programming Language :: Python :: 3.10", 47 | "Programming Language :: Python :: 3.11", 48 | "Programming Language :: Python :: 3.12", 49 | "Programming Language :: Python :: 3.13", 50 | ] 51 | dynamic = ["version"] 52 | 53 | [project.optional-dependencies] 54 | django = ["Django>=4.2"] 55 | django-filter = ["django-filter>=24.2"] 56 | djangorestframework = ["djangorestframework>=3.10.3,<3.17"] 57 | pydantic = ["pydantic>=2.0"] 58 | 59 | [project.urls] 60 | Homepage = "https://github.com/fyntex/lib-cl-sii-python" 61 | Changelog = "https://github.com/fyntex/lib-cl-sii-python/blob/develop/HISTORY.md" 62 | 63 | [tool.setuptools] 64 | include-package-data = true 65 | zip-safe = false 66 | 67 | [tool.setuptools.packages.find] 68 | where = ["src"] 69 | include = ["*"] 70 | exclude = [ 71 | "scripts", 72 | "tests*", 73 | ] 74 | namespaces = true 75 | 76 | [tool.setuptools.package-data] 77 | # note: the "typing information" of this project's packages is not made available to its users 78 | # automatically; it needs to be packaged and distributed. The way to do so is fairly new and 79 | # it is specified in PEP 561 - "Distributing and Packaging Type Information". 80 | # See: 81 | # - https://www.python.org/dev/peps/pep-0561/#packaging-type-information 82 | # - https://github.com/python/typing/issues/84 83 | # - https://github.com/python/mypy/issues/3930 84 | # warning: remember to replicate this in the manifest file for source distribution ('MANIFEST.in'). 85 | cl_sii = [ 86 | # Indicates that the "typing information" of the package should be distributed. 87 | "py.typed", 88 | # Data files that are not in a sub-package. 89 | "data/cte/schemas-json/*.schema.json", 90 | "data/ref/factura_electronica/schemas-xml/*.xsd", 91 | ] 92 | 93 | [tool.setuptools.dynamic] 94 | version = {attr = "cl_sii.__version__"} 95 | 96 | [tool.distutils.bdist_wheel] 97 | universal = false 98 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ij2Qn6xOc2eRx3hwyO/GrzptoBk= 12 | 13 | 14 | 15 | fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk 16 | ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi 17 | wqSxDcYjTT6vXsLPrZk= 18 | 19 | 20 | 21 | 22 | 23 | pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl 24 | 9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN 25 | Uavs/9J+gR9BBMs/eYE= 26 | 27 | AQAB 28 | 29 | 30 | 31 | 32 | MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx 33 | HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE 34 | ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD 35 | EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW 36 | GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa 37 | MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh 38 | MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE 39 | CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM 40 | b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI 41 | hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx 42 | O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC 43 | /z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 44 | FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV 45 | HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S 46 | ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu 47 | Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 48 | Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 49 | MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC 50 | AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu 51 | Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp 52 | AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A 53 | IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk 54 | AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA 55 | cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA 56 | mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD 57 | qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un 58 | +/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud 59 | B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY 60 | +kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/scripts/clean_dte_xml_file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Clean DTE XML files. 4 | 5 | 6 | Example for a single file:: 7 | 8 | ./scripts/clean_dte_xml_file.py file \ 9 | 'tests/test_data/sii-dte/DTE--76354771-K--33--170.xml' \ 10 | 'tests/test_data/sii-dte/DTE--76354771-K--33--170-clean.xml' 11 | 12 | 13 | Example for all files in a directory:: 14 | 15 | ./scripts/clean_dte_xml_file.py dir 'tests/test_data/sii-dte/' 16 | 17 | 18 | """ 19 | import difflib 20 | import os 21 | import pathlib 22 | import sys 23 | from typing import Iterable 24 | 25 | 26 | try: 27 | import cl_sii # noqa: F401 28 | except ImportError: 29 | # If package 'cl-sii' is not installed, try appending the project repo directory to the 30 | # Python path, assuming thath we are in the project repo. If not, it will fail nonetheless. 31 | sys.path.append(os.path.dirname(os.path.abspath(__name__))) 32 | import cl_sii # noqa: F401 33 | 34 | import cl_sii.dte.parse 35 | from cl_sii.libs import xml_utils 36 | 37 | 38 | # TODO: log messages instead of print. 39 | 40 | 41 | def clean_dte_xml_file(input_file_path: str, output_file_path: str) -> Iterable[bytes]: 42 | with open(input_file_path, mode='rb') as f: 43 | file_bytes = f.read() 44 | 45 | xml_doc = xml_utils.parse_untrusted_xml(file_bytes) 46 | 47 | xml_doc_cleaned, modified = cl_sii.dte.parse.clean_dte_xml( 48 | xml_doc, 49 | set_missing_xmlns=True, 50 | remove_doc_personalizado=True, 51 | ) 52 | 53 | # TODO: add exception with a nice message for the caller. 54 | cl_sii.dte.parse.validate_dte_xml(xml_doc_cleaned) 55 | 56 | with open(output_file_path, 'w+b') as f: 57 | xml_utils.write_xml_doc(xml_doc_cleaned, f) 58 | 59 | with open(output_file_path, mode='rb') as f: 60 | file_bytes_rewritten = f.read() 61 | 62 | # note: another way to compute the difference in a similar format is 63 | # `diff -Naur $input_file_path $output_file_path` 64 | file_bytes_diff_gen = difflib.diff_bytes( 65 | dfunc=difflib.unified_diff, a=file_bytes.splitlines(), b=file_bytes_rewritten.splitlines() 66 | ) 67 | 68 | return file_bytes_diff_gen 69 | 70 | 71 | def main_single_file(input_file_path: str, output_file_path: str) -> None: 72 | file_bytes_diff_gen = clean_dte_xml_file( 73 | input_file_path=input_file_path, output_file_path=output_file_path 74 | ) 75 | 76 | for diff_line in file_bytes_diff_gen: 77 | print(diff_line) 78 | 79 | 80 | def main_dir_files(input_files_dir_path: str) -> None: 81 | for p in pathlib.Path(input_files_dir_path).iterdir(): 82 | if not p.is_file(): 83 | continue 84 | 85 | # e.g. 'an example.xml' -> 'an example.clean.xml' 86 | input_file_path = str(p) 87 | output_file_path = str(p.with_suffix(f'.clean{p.suffix}')) 88 | 89 | print(f"\n\nWill clean file '{input_file_path}' and save it to '{output_file_path}'.") 90 | file_bytes_diff_gen = clean_dte_xml_file( 91 | input_file_path=input_file_path, output_file_path=output_file_path 92 | ) 93 | 94 | print("Difference between input and output files:") 95 | diff_line = None 96 | for diff_line in file_bytes_diff_gen: 97 | print(diff_line) 98 | if diff_line is None: 99 | print("No difference.") 100 | 101 | 102 | if __name__ == '__main__': 103 | if sys.argv[1] == 'file': 104 | main_single_file(input_file_path=sys.argv[2], output_file_path=sys.argv[3]) 105 | elif sys.argv[1] == 'dir': 106 | main_dir_files(input_files_dir_path=sys.argv[2]) 107 | else: 108 | raise ValueError(f"Invalid option: '{sys.argv[1]}'") 109 | -------------------------------------------------------------------------------- /src/cl_sii/rtc/xml_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import Any, ClassVar, Optional 5 | 6 | import signxml 7 | import signxml.util 8 | import signxml.verifier 9 | 10 | from cl_sii.dte.parse import DTE_XMLNS_MAP 11 | from cl_sii.libs import crypto_utils, xml_utils 12 | from .data_models_aec import AecXml 13 | 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class AecXMLVerifier(signxml.verifier.XMLVerifier): 19 | """ 20 | Custom XML Signature Verifier for AECs. 21 | """ 22 | 23 | AEC_XML_ELEMENT_TAG: ClassVar[str] = '{{{namespace}}}{tag}'.format( 24 | namespace=DTE_XMLNS_MAP['sii-dte'], 25 | tag='AEC', 26 | ) 27 | 28 | def _get_signature(self, root: Any) -> object: 29 | if root.tag != self.AEC_XML_ELEMENT_TAG: 30 | raise ValueError( 31 | f'Only XML element {self.AEC_XML_ELEMENT_TAG!r} is supported. Found: {root.tag!r}', 32 | ) 33 | 34 | if root.tag == signxml.util.ds_tag("Signature"): 35 | return root 36 | else: 37 | return self._find(root, "Signature") 38 | 39 | 40 | ############################################################################### 41 | # functions 42 | ############################################################################### 43 | 44 | 45 | def verify_aec_signature( 46 | aec_xml_doc: xml_utils.XmlElement, 47 | aec_xml: AecXml, 48 | ) -> Optional[bool]: 49 | """ 50 | Verify signature of AEC XML document ``aec_xml_doc``. 51 | 52 | :param aec_xml_doc: An AEC XML document, as returned by ``xml_utils.parse_untrusted_xml()``. 53 | :param aec_xml: An instance of ``data_models_aec.AecXml`` with the data in the "cesión"'s 54 | AEC XML document parsed from `aec_xml_doc` by ``parse_aec.parse_aec_xml``. 55 | :raises ValueError: If the attribute `signature_x509_cert_der` of the AecXml is None. 56 | :raises Exception: on unrecoverable errors 57 | """ 58 | signature_verified: Optional[bool] 59 | signature_x509_cert: Optional[crypto_utils.X509Cert] 60 | 61 | if aec_xml.signature_x509_cert_der is None: 62 | raise ValueError("Field 'signature_x509_cert_der' can not be None.") 63 | 64 | try: 65 | signature_x509_cert = crypto_utils.load_der_x509_cert( 66 | aec_xml.signature_x509_cert_der, 67 | ) 68 | except ValueError: 69 | signature_verified = None 70 | logger.debug( 71 | "The X.509 certificate could not be loaded from AEC's digital " 72 | "signature's DER-encoded X.509 certificate." 73 | ) 74 | return signature_verified 75 | 76 | try: 77 | aec_xml_verifier = AecXMLVerifier() 78 | 79 | # Workaround for breaking change in signxml 2.10.0 and 2.10.1: 80 | # (See https://github.com/XML-Security/signxml/blob/v2.10.1/Changes.rst) 81 | aec_xml_verifier.excise_empty_xmlns_declarations = True 82 | 83 | xml_utils.verify_xml_signature( 84 | aec_xml_doc, 85 | trusted_x509_cert=signature_x509_cert, 86 | xml_verifier=aec_xml_verifier, 87 | xml_verifier_supports_multiple_signatures=True, 88 | ) 89 | except xml_utils.XmlSignatureUnverified: 90 | signature_verified = False 91 | logger.debug("AEC's digital signature did not verify") 92 | except xml_utils.XmlSignatureInvalid: 93 | signature_verified = False 94 | logger.debug("AEC's digital signature is invalid") 95 | except Exception: 96 | signature_verified = None 97 | logger.exception( 98 | "Unexpected error when trying to verify digital signature of XML document." 99 | ) 100 | else: 101 | signature_verified = True 102 | 103 | return signature_verified 104 | -------------------------------------------------------------------------------- /src/scripts/canonicalize_xml_file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Canonicalize XML files. 4 | 5 | 6 | Convert XML to its C14N 2.0 serialised form (see also: https://www.w3.org/TR/xml-c14n2/). 7 | 8 | 9 | Example for a single file:: 10 | 11 | ./scripts/canonicalize_xml_file.py file \ 12 | 'tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml' \ 13 | 'tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2-canonicalized-c14n.xml' 14 | 15 | 16 | Example for all files in a directory:: 17 | 18 | ./scripts/canonicalize_xml_file.py dir 'tests/test_data/sii-rtc/' 19 | """ 20 | 21 | from __future__ import annotations 22 | 23 | import difflib 24 | import os 25 | import pathlib 26 | import sys 27 | import xml.etree.ElementTree 28 | from typing import BinaryIO, Iterable, TextIO, Union 29 | 30 | 31 | try: 32 | import cl_sii # noqa: F401 33 | except ImportError: 34 | # If package 'cl-sii' is not installed, try appending the project repo directory to the 35 | # Python path, assuming thath we are in the project repo. If not, it will fail nonetheless. 36 | sys.path.append(os.path.dirname(os.path.abspath(__name__))) 37 | import cl_sii # noqa: F401 38 | 39 | 40 | # TODO: log messages instead of print. 41 | 42 | 43 | def canonicalize_xml_file( 44 | input_file_path: pathlib.Path, 45 | output_file_path: pathlib.Path, 46 | ) -> Iterable[bytes]: 47 | if sys.version_info < (3, 8): 48 | raise NotImplementedError('Python version ≥ 3.8 required') 49 | 50 | f: Union[TextIO, BinaryIO] 51 | 52 | with open(input_file_path, mode='rb') as f: 53 | file_bytes = f.read() 54 | 55 | with open(output_file_path, 'wt') as f: 56 | xml.etree.ElementTree.canonicalize(xml_data=file_bytes, out=f) 57 | 58 | with open(output_file_path, mode='rb') as f: 59 | file_bytes_rewritten = f.read() 60 | 61 | # Note: Another way to compute the difference in a similar format is 62 | # `diff -Naur $input_file_path $output_file_path` 63 | file_bytes_diff_gen = difflib.diff_bytes( 64 | dfunc=difflib.unified_diff, 65 | a=file_bytes.splitlines(), 66 | b=file_bytes_rewritten.splitlines(), 67 | ) 68 | 69 | return file_bytes_diff_gen 70 | 71 | 72 | def main_single_file(input_file_path: pathlib.Path, output_file_path: pathlib.Path) -> None: 73 | file_bytes_diff_gen = canonicalize_xml_file( 74 | input_file_path=input_file_path, 75 | output_file_path=output_file_path, 76 | ) 77 | 78 | for diff_line in file_bytes_diff_gen: 79 | print(diff_line) 80 | 81 | 82 | def main_dir_files(input_files_dir_path: pathlib.Path) -> None: 83 | for p in input_files_dir_path.iterdir(): 84 | if not p.is_file(): 85 | continue 86 | 87 | # e.g. 'an example.xml' -> 'an example.clean.xml' 88 | input_file_path = p 89 | output_file_path = p.with_suffix(f'.clean{p.suffix}') 90 | 91 | print(f"\n\nWill clean file '{input_file_path}' and save it to '{output_file_path}'.") 92 | file_bytes_diff_gen = canonicalize_xml_file( 93 | input_file_path=input_file_path, 94 | output_file_path=output_file_path, 95 | ) 96 | 97 | print("Difference between input and output files:") 98 | diff_line = None 99 | for diff_line in file_bytes_diff_gen: 100 | print(diff_line) 101 | if diff_line is None: 102 | print("No difference.") 103 | 104 | 105 | if __name__ == '__main__': 106 | if sys.argv[1] == 'file': 107 | main_single_file( 108 | input_file_path=pathlib.Path(sys.argv[2]), 109 | output_file_path=pathlib.Path(sys.argv[3]), 110 | ) 111 | elif sys.argv[1] == 'dir': 112 | main_dir_files( 113 | input_files_dir_path=pathlib.Path(sys.argv[2]), 114 | ) 115 | else: 116 | raise ValueError(f"Invalid option: '{sys.argv[1]}'") 117 | -------------------------------------------------------------------------------- /src/tests/test_rut_constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import unittest 5 | from typing import ClassVar, Pattern 6 | 7 | import jsonschema 8 | 9 | from cl_sii.rut import constants 10 | 11 | 12 | class RutDigitsConstantsTestCase(unittest.TestCase): 13 | RUT_DIGITS_MIN_VALUE: ClassVar[int] 14 | RUT_DIGITS_MAX_VALUE: ClassVar[int] 15 | PERSONA_JURIDICA_MIN_RUT_DIGITS: ClassVar[int] 16 | 17 | @classmethod 18 | def setUpClass(cls) -> None: 19 | super().setUpClass() 20 | 21 | cls.RUT_DIGITS_MIN_VALUE = constants.RUT_DIGITS_MIN_VALUE 22 | cls.RUT_DIGITS_MAX_VALUE = constants.RUT_DIGITS_MAX_VALUE 23 | cls.PERSONA_JURIDICA_MIN_RUT_DIGITS = constants.PERSONA_JURIDICA_MIN_RUT_DIGITS 24 | 25 | def test_min_value(self) -> None: 26 | min_rut_digits = self.RUT_DIGITS_MIN_VALUE 27 | 28 | self.assertLessEqual(min_rut_digits, self.RUT_DIGITS_MAX_VALUE) 29 | 30 | def test_max_value(self) -> None: 31 | max_rut_digits = self.RUT_DIGITS_MAX_VALUE 32 | 33 | self.assertGreaterEqual(max_rut_digits, self.RUT_DIGITS_MIN_VALUE) 34 | 35 | def test_persona_juridica_min_value(self) -> None: 36 | min_rut_digits = self.PERSONA_JURIDICA_MIN_RUT_DIGITS 37 | 38 | self.assertGreaterEqual(min_rut_digits, self.RUT_DIGITS_MIN_VALUE) 39 | self.assertLessEqual(min_rut_digits, self.RUT_DIGITS_MAX_VALUE) 40 | 41 | 42 | class RutRegexConstantsTestCase(unittest.TestCase): 43 | RUT_CANONICAL_STRICT_REGEX: ClassVar[Pattern[str]] 44 | RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX: ClassVar[Pattern[str]] 45 | 46 | @classmethod 47 | def setUpClass(cls) -> None: 48 | super().setUpClass() 49 | 50 | cls.RUT_CANONICAL_STRICT_REGEX = constants.RUT_CANONICAL_STRICT_REGEX 51 | cls.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX = ( 52 | constants.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX 53 | ) 54 | 55 | def test_json_schema_regex_is_python_regex_without_named_groups(self) -> None: 56 | # %% -----Arrange----- 57 | 58 | python_regex = self.RUT_CANONICAL_STRICT_REGEX 59 | python_regex_without_named_groups = re.compile( 60 | re.sub( 61 | pattern=r'\?P<\w+>', 62 | repl='', 63 | string=python_regex.pattern, 64 | ) 65 | ) 66 | expected = python_regex_without_named_groups 67 | 68 | # %% -----Act----- 69 | 70 | actual = self.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX 71 | 72 | # %% -----Assert----- 73 | 74 | self.assertEqual(expected, actual) 75 | 76 | # %% ----- 77 | 78 | def test_json_schema_regex_is_valid_schema(self) -> None: 79 | # %% -----Arrange----- 80 | 81 | schema = { 82 | "type": "string", 83 | "pattern": self.RUT_CANONICAL_STRICT_JSON_SCHEMA_REGEX.pattern, 84 | } 85 | valid_test_values = [ 86 | '0-0', 87 | '1-9', 88 | '6-K', 89 | '78773510-K', 90 | ] 91 | invalid_test_values = [ 92 | '6K', 93 | '6-k', 94 | '78773510-k', 95 | '78.773.510-K', 96 | 78773510, 97 | 1.9, 98 | None, 99 | ] 100 | 101 | # %% -----Act & Assert----- 102 | 103 | for test_value in valid_test_values: 104 | with self.subTest(test_value=test_value): 105 | try: 106 | jsonschema.validate(instance=test_value, schema=schema) 107 | except jsonschema.exceptions.ValidationError as exc: 108 | self.fail(f'{exc.__class__.__name__} raised') 109 | 110 | for invalid_test_value in invalid_test_values: 111 | with self.subTest(test_value=invalid_test_value): 112 | with self.assertRaises(jsonschema.exceptions.ValidationError): 113 | jsonschema.validate(instance=invalid_test_value, schema=schema) 114 | 115 | # %% ----- 116 | -------------------------------------------------------------------------------- /src/tests/test_extras_dj_url_converters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | from typing import Sequence, Tuple 5 | 6 | from cl_sii.dte.constants import TipoDte 7 | from cl_sii.extras.dj_url_converters import RutConverter, TipoDteConverter 8 | from cl_sii.rut import Rut 9 | 10 | 11 | class RutConverterTest(unittest.TestCase): 12 | """ 13 | Tests for :class:`RutConverter`. 14 | """ 15 | 16 | def test_regex(self) -> None: 17 | for rut_str in ( 18 | '60805000-0', 19 | '78773510-K', 20 | '123456-0', 21 | '123-6', 22 | '1-9', 23 | '6-K', 24 | ): 25 | self.assertRegex(rut_str, RutConverter.regex) 26 | 27 | def test_regex_matches_leading_zeroes(self) -> None: 28 | for rut_str in ('001-9', '000006-K', '0123456-0'): 29 | self.assertRegex(rut_str, RutConverter.regex) 30 | 31 | def test_regex_matches_uppercase_and_lowercase(self) -> None: 32 | self.assertRegex('6-K', RutConverter.regex) 33 | self.assertRegex('6-k', RutConverter.regex) 34 | 35 | def test_to_python(self) -> None: 36 | obj = RutConverter() 37 | 38 | test_values: Sequence[Tuple[str, Rut]] = [ 39 | ('60805000-0', Rut('60805000-0')), 40 | ('78773510-K', Rut('78773510-K')), 41 | ('123456-0', Rut('123456-0')), 42 | ('1-9', Rut('1-9')), 43 | ('01-9', Rut('1-9')), 44 | ('6-k', Rut('6-K')), 45 | ] 46 | 47 | for string_test_value, python_test_value in test_values: 48 | self.assertEqual(obj.to_python(string_test_value), python_test_value) 49 | 50 | def test_to_url(self) -> None: 51 | obj = RutConverter() 52 | 53 | test_values: Sequence[Tuple[Rut, str]] = [ 54 | (Rut('60805000-0'), '60805000-0'), 55 | (Rut('78773510-K'), '78773510-K'), 56 | (Rut('123456-0'), '123456-0'), 57 | (Rut('1-9'), '1-9'), 58 | ] 59 | 60 | for python_test_value, string_test_value in test_values: 61 | self.assertEqual(obj.to_url(python_test_value), string_test_value) 62 | 63 | 64 | class TipoDteConverterTest(unittest.TestCase): 65 | """ 66 | Tests for :class:`TipoDteConverter`. 67 | """ 68 | 69 | def test_regex(self) -> None: 70 | for tipo_dte_str in ( 71 | '33', 72 | '34', 73 | '43', 74 | '46', 75 | '52', 76 | '56', 77 | '61', 78 | '110', 79 | ): 80 | self.assertRegex(tipo_dte_str, TipoDteConverter.regex) 81 | 82 | def test_to_python(self) -> None: 83 | obj = TipoDteConverter() 84 | 85 | test_values: Sequence[Tuple[str, TipoDte]] = [ 86 | ('33', TipoDte(33)), 87 | ('34', TipoDte(34)), 88 | ('43', TipoDte(43)), 89 | ('46', TipoDte(46)), 90 | ('52', TipoDte(52)), 91 | ('56', TipoDte(56)), 92 | ('61', TipoDte(61)), 93 | # warning: `110` is a valid value, but `TipoDte` doesn't support it yet. 94 | # ('110', TipoDte(110)), 95 | ] 96 | 97 | for string_test_value, python_test_value in test_values: 98 | self.assertEqual(obj.to_python(string_test_value), python_test_value) 99 | 100 | def test_to_url(self) -> None: 101 | obj = TipoDteConverter() 102 | 103 | test_values: Sequence[Tuple[TipoDte, str]] = [ 104 | (TipoDte(33), '33'), 105 | (TipoDte(34), '34'), 106 | (TipoDte(43), '43'), 107 | (TipoDte(46), '46'), 108 | (TipoDte(52), '52'), 109 | (TipoDte(56), '56'), 110 | (TipoDte(61), '61'), 111 | # warning: `110` is a valid value, but `TipoDte` doesn't support it yet. 112 | # (TipoDte(110), '110'), 113 | ] 114 | 115 | for python_test_value, string_test_value in test_values: 116 | self.assertEqual(obj.to_url(python_test_value), string_test_value) 117 | -------------------------------------------------------------------------------- /src/cl_sii/libs/dataclass_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dataclass utils 3 | =============== 4 | 5 | Utils for std lib's :class:`dataclasses.Dataclass` classes and instances. 6 | 7 | """ 8 | 9 | import dataclasses 10 | import enum 11 | 12 | 13 | @enum.unique 14 | class DcDeepComparison(enum.IntEnum): 15 | """ 16 | The possible results of a "deep comparison" between 2 dataclass instances. 17 | 18 | .. warning:: The type of both instances must be the same. 19 | 20 | For dataclass instances ``instance_1`` and ``instance_2``, the 21 | enum member name should be interpreted as: 22 | ``instance_1`` is ``enum_member_name`` of|to|with ``instance_2`` 23 | e.g. ``instance_1`` is subset of ``instance_2``. 24 | 25 | .. note:: The enum members values are arbitrary. 26 | 27 | """ 28 | 29 | EQUAL = 0 30 | """ 31 | For each dataclass attribute A and B have the same value. 32 | """ 33 | 34 | SUBSET = 11 35 | """ 36 | A and B are not equal, and A's value for each dataclass attribute whose 37 | value is not None is equal to B's value for the same attribute. 38 | """ 39 | 40 | SUPERSET = 12 41 | """ 42 | A and B are not equal, and B's value for each dataclass attribute whose 43 | value is not None is equal to A's value for the same attribute. 44 | """ 45 | 46 | CONFLICTED = -1 47 | """ 48 | For one or more dataclass attributes A and B have a different value. 49 | """ 50 | 51 | 52 | class DcDeepCompareMixin: 53 | """ 54 | Mixin for dataclass instances "deep comparison". 55 | """ 56 | 57 | def deep_compare_to(self, value: object) -> DcDeepComparison: 58 | """ 59 | Return result of a "deep comparison" against another dataclass instance. 60 | """ 61 | # note: 'is_dataclass' returns True if obj is a dataclass or an instance of a dataclass. 62 | if not dataclasses.is_dataclass(self): 63 | # TODO: would it be possible to run this check when the **class** is created? 64 | raise Exception( 65 | "Programming error. Only dataclasses may subclass 'DcDeepCompareMixin'." 66 | ) 67 | # note: 'is_dataclass' returns True if obj is a dataclass or an instance of a dataclass. 68 | if not dataclasses.is_dataclass(value): 69 | raise TypeError("Value must be a dataclass instance.") 70 | 71 | return _dc_deep_compare_to(self, value) 72 | 73 | 74 | def dc_deep_compare(value_a: object, value_b: object) -> DcDeepComparison: 75 | """ 76 | Return result of a "deep comparison" between dataclass instances. 77 | """ 78 | # note: 'is_dataclass' returns True if obj is a dataclass or an instance of a dataclass. 79 | if not dataclasses.is_dataclass(value_a) or not dataclasses.is_dataclass(value_b): 80 | raise TypeError("Values must be dataclass instances.") 81 | 82 | return _dc_deep_compare_to(value_a, value_b) 83 | 84 | 85 | def _dc_deep_compare_to(value_a: object, value_b: object) -> DcDeepComparison: 86 | if type(value_a) is not type(value_b): 87 | raise TypeError("Values to be compared must be of the same type.") 88 | 89 | if value_a == value_b: 90 | return DcDeepComparison.EQUAL 91 | 92 | # Remove dataclass attributes whose value is None. 93 | self_dict_clean = { 94 | k: v 95 | for k, v in dataclasses.asdict(value_a).items() # type: ignore[call-overload] 96 | if v is not None 97 | } 98 | value_dict_clean = { 99 | k: v 100 | for k, v in dataclasses.asdict(value_b).items() # type: ignore[call-overload] 101 | if v is not None 102 | } 103 | 104 | if len(self_dict_clean) < len(value_dict_clean): 105 | for k, v in self_dict_clean.items(): 106 | if v != value_dict_clean[k]: 107 | return DcDeepComparison.CONFLICTED 108 | return DcDeepComparison.SUBSET 109 | else: 110 | for k, v in value_dict_clean.items(): 111 | if v != self_dict_clean[k]: 112 | return DcDeepComparison.CONFLICTED 113 | return DcDeepComparison.SUPERSET 114 | -------------------------------------------------------------------------------- /src/tests/test_extras_dj_filters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import unittest 4 | 5 | import django_filters 6 | 7 | from cl_sii.extras import dj_form_fields, dj_model_fields 8 | from cl_sii.extras.dj_filters import RutFilter, SiiFilterSet 9 | 10 | 11 | class RutFilterTest(unittest.TestCase): 12 | """ 13 | Tests for :class:`cl_sii.extras.dj_filters.RutFilter`. 14 | """ 15 | 16 | @unittest.expectedFailure 17 | def test_new_instance(self) -> None: 18 | try: 19 | filter = RutFilter() 20 | except Exception as exc: 21 | self.fail(f'{exc.__class__.__name__} raised') 22 | 23 | self.assertIsInstance(filter, RutFilter) 24 | self.assertIsInstance(filter, django_filters.filters.Filter) 25 | 26 | def test_filter_class_lookup_expressions(self) -> None: 27 | expected_field_class = dj_form_fields.RutField 28 | for lookup_expr in [ 29 | 'exact', 30 | 'iexact', 31 | 'in', 32 | 'gt', 33 | 'gte', 34 | 'lt', 35 | 'lte', 36 | ]: 37 | with self.subTest(field_class=expected_field_class, lookup_expr=lookup_expr): 38 | filter_instance = RutFilter(lookup_expr=lookup_expr) 39 | self.assertIs(filter_instance.field_class, expected_field_class) 40 | 41 | expected_field_class = django_filters.CharFilter.field_class 42 | for lookup_expr in [ 43 | 'contains', 44 | 'icontains', 45 | 'startswith', 46 | 'istartswith', 47 | 'endswith', 48 | 'iendswith', 49 | ]: 50 | with self.subTest(field_class=expected_field_class, lookup_expr=lookup_expr): 51 | filter_instance = RutFilter(lookup_expr=lookup_expr) 52 | self.assertIs(filter_instance.field_class, expected_field_class) 53 | 54 | # TODO: Add tests. 55 | 56 | 57 | class SiiFilterSetTest(unittest.TestCase): 58 | """ 59 | Tests for :class:`cl_sii.extras.dj_filters.SiiFilterSet`. 60 | """ 61 | 62 | @unittest.skip("TODO: Implement for 'filter_for_lookup'.") 63 | def test_filter_for_lookup(self) -> None: 64 | assert SiiFilterSet.filter_for_lookup() 65 | 66 | def test_filter_for_lookup_types(self) -> None: 67 | field = dj_model_fields.RutField() 68 | 69 | expected_field_class = dj_form_fields.RutField 70 | for lookup_type in [ 71 | 'exact', 72 | 'iexact', 73 | 'gt', 74 | 'gte', 75 | 'lt', 76 | 'lte', 77 | ]: 78 | with self.subTest(field_class=expected_field_class, lookup_type=lookup_type): 79 | filter_class, params = SiiFilterSet.filter_for_lookup(field, lookup_type) 80 | filter_instance = filter_class(**{'lookup_expr': lookup_type, **params}) 81 | self.assertIs(filter_instance.field_class, expected_field_class) 82 | 83 | for lookup_type in [ 84 | 'in', 85 | ]: 86 | with self.subTest(field_class=expected_field_class, lookup_type=lookup_type): 87 | filter_class, params = SiiFilterSet.filter_for_lookup(field, lookup_type) 88 | filter_instance = filter_class(**{'lookup_expr': lookup_type, **params}) 89 | self.assertTrue(issubclass(filter_instance.field_class, expected_field_class)) 90 | 91 | expected_field_class = django_filters.CharFilter.field_class 92 | for lookup_type in [ 93 | 'contains', 94 | 'icontains', 95 | 'startswith', 96 | 'istartswith', 97 | 'endswith', 98 | 'iendswith', 99 | ]: 100 | with self.subTest(field_class=expected_field_class, lookup_type=lookup_type): 101 | filter_class, params = SiiFilterSet.filter_for_lookup(field, lookup_type) 102 | filter_instance = filter_class(**{'lookup_expr': lookup_type, **params}) 103 | self.assertIs(filter_instance.field_class, expected_field_class) 104 | 105 | # TODO: Add tests. 106 | -------------------------------------------------------------------------------- /src/tests/test_extras_dj_form_fields.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import django.core.exceptions 4 | 5 | from cl_sii.extras.dj_form_fields import Rut, RutField 6 | 7 | 8 | class RutFieldTest(unittest.TestCase): 9 | valid_rut_canonical: str 10 | valid_rut_instance: Rut 11 | valid_rut_verbose_leading_zero_lowercase: str 12 | 13 | @classmethod 14 | def setUpClass(cls) -> None: 15 | cls.invalid_rut_canonical = '60803000K' 16 | cls.valid_rut_canonical = '60803000-K' 17 | cls.valid_rut_instance = Rut(cls.valid_rut_canonical) 18 | cls.valid_rut_canonical_with_invalid_dv = '60803000-0' 19 | cls.valid_rut_canonical_instance_with_invalid_dv = Rut( 20 | cls.valid_rut_canonical_with_invalid_dv 21 | ) 22 | assert not cls.valid_rut_canonical_instance_with_invalid_dv.validate_dv() 23 | cls.valid_rut_verbose_leading_zero_lowercase = '060.803.000-k' 24 | 25 | def test_clean_value_of_invalid_canonical_str(self) -> None: 26 | rut_field = RutField() 27 | with self.assertRaises(django.core.exceptions.ValidationError) as cm: 28 | rut_field.clean(self.invalid_rut_canonical) 29 | self.assertEqual(cm.exception.code, 'invalid') 30 | 31 | def test_clean_value_of_canonical_str(self) -> None: 32 | rut_field = RutField() 33 | cleaned_value = rut_field.clean(self.valid_rut_canonical) 34 | self.assertIsInstance(cleaned_value, Rut) 35 | self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical) 36 | 37 | def test_clean_value_of_non_canonical_str(self) -> None: 38 | rut_field = RutField() 39 | cleaned_value = rut_field.clean(self.valid_rut_verbose_leading_zero_lowercase) 40 | self.assertIsInstance(cleaned_value, Rut) 41 | self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical) 42 | 43 | def test_clean_value_of_Rut(self) -> None: 44 | rut_field = RutField() 45 | cleaned_value = rut_field.clean(self.valid_rut_instance) 46 | self.assertIsInstance(cleaned_value, Rut) 47 | self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical) 48 | 49 | def test_clean_value_of_rut_str_with_invalid_dv_if_validated(self) -> None: 50 | rut_field = RutField(validate_dv=True) 51 | with self.assertRaises(django.core.exceptions.ValidationError) as cm: 52 | rut_field.clean(self.valid_rut_canonical_with_invalid_dv) 53 | self.assertEqual(cm.exception.code, 'invalid_dv') 54 | 55 | def test_clean_value_of_rut_str_with_invalid_dv_if_not_validated(self) -> None: 56 | rut_field = RutField(validate_dv=False) 57 | cleaned_value = rut_field.clean(self.valid_rut_canonical_with_invalid_dv) 58 | self.assertIsInstance(cleaned_value, Rut) 59 | self.assertEqual(cleaned_value.canonical, self.valid_rut_canonical_with_invalid_dv) 60 | 61 | def test_clean_value_of_rut_instance_with_invalid_dv_if_validated(self) -> None: 62 | rut_field = RutField(validate_dv=True) 63 | with self.assertRaises(django.core.exceptions.ValidationError) as cm: 64 | rut_field.clean(self.valid_rut_canonical_instance_with_invalid_dv) 65 | self.assertEqual(cm.exception.code, 'invalid_dv') 66 | 67 | def test_clean_value_of_rut_instance_with_invalid_dv_if_not_validated(self) -> None: 68 | rut_field = RutField(validate_dv=False) 69 | cleaned_value = rut_field.clean(self.valid_rut_canonical_instance_with_invalid_dv) 70 | self.assertIsInstance(cleaned_value, Rut) 71 | self.assertEqual(cleaned_value, self.valid_rut_canonical_instance_with_invalid_dv) 72 | 73 | def test_clean_of_empty_value_if_not_required(self) -> None: 74 | rut_field = RutField(required=False) 75 | for value in RutField.empty_values: 76 | cleaned_value = rut_field.clean(value) 77 | self.assertIsNone(cleaned_value) 78 | 79 | def test_clean_of_empty_value_if_required(self) -> None: 80 | rut_field = RutField() 81 | for value in RutField.empty_values: 82 | with self.assertRaises(django.core.exceptions.ValidationError) as cm: 83 | rut_field.clean(value) 84 | self.assertEqual(cm.exception.code, 'required') 85 | -------------------------------------------------------------------------------- /src/cl_sii/data/cte/schemas-json/f29_datos_obj.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "SII CTE Form 29 'datos' Object", 3 | "description": "Schema of (a subset of) the 'datos' JavaScript object embedded in the IFrames of the HTML version of the CTE's 'Declaraciones de IVA (F29)'.", 4 | "type": "object", 5 | "properties": { 6 | "campos": { 7 | "type": "object", 8 | "patternProperties": { 9 | "^\\d{3,4}$": { 10 | "type": "string" 11 | } 12 | }, 13 | "additionalProperties": false 14 | }, 15 | "extras": { 16 | "type": "object", 17 | "properties": { 18 | "BANCO": { 19 | "type": "string" 20 | }, 21 | "CALIDADDECLARANTE": { 22 | "type": "string" 23 | }, 24 | "CARTRIB": { 25 | "type": "string" 26 | }, 27 | "CLASE": { 28 | "type": "string" 29 | }, 30 | "CODINT": { 31 | "type": "string", 32 | "pattern": "^\\d+$" 33 | }, 34 | "COD_FORM": { 35 | "type": "string", 36 | "enum": [ 37 | "F29" 38 | ] 39 | }, 40 | "DV_ORIGEN": { 41 | "type": "string", 42 | "pattern": "^(\\d|K)$" 43 | }, 44 | "FECHA_HOY": { 45 | "type": "string", 46 | "pattern": "^\\d{1,2}/\\d{1,2}/\\d{4}$" 47 | }, 48 | "FECHA_INGRESO": { 49 | "type": "string", 50 | "pattern": "^\\d{1,2}\\d{1,2}\\d{4}$" 51 | }, 52 | "FOLIO": { 53 | "type": "string", 54 | "pattern": "^\\d+$" 55 | }, 56 | "MEDIO_INGRESO": { 57 | "type": "string" 58 | }, 59 | "MEDIO_PAGO": { 60 | "type": "string" 61 | }, 62 | "MOSTRAR_FOLIO": { 63 | "type": "string" 64 | }, 65 | "NOMBRES": { 66 | "type": "string" 67 | }, 68 | "OPCION": { 69 | "type": "string" 70 | }, 71 | "PERIODO": { 72 | "type": "string", 73 | "pattern": "^\\d{4}\\d{2}$" 74 | }, 75 | "RUT_ORIGEN": { 76 | "type": "string", 77 | "pattern": "^\\d+$" 78 | }, 79 | "TIPO_MOVIMIENTO": { 80 | "type": "string" 81 | }, 82 | "TOKEN": { 83 | "type": "string" 84 | }, 85 | "USUARIO": { 86 | "type": "string" 87 | }, 88 | "cant_anuladas": { 89 | "type": "integer" 90 | } 91 | }, 92 | "patternProperties": { 93 | ".+": { 94 | "type": [ 95 | "number", 96 | "string" 97 | ] 98 | } 99 | }, 100 | "required": [ 101 | "PERIODO" 102 | ], 103 | "additionalProperties": false 104 | }, 105 | "folioF": { 106 | "type": "object" 107 | }, 108 | "glosa": { 109 | "type": "object", 110 | "patternProperties": { 111 | "^\\d{3,4}$": { 112 | "type": "string" 113 | } 114 | }, 115 | "additionalProperties": false 116 | }, 117 | "justif": { 118 | "type": "object", 119 | "patternProperties": { 120 | "^\\d{3,4}$": { 121 | "type": "string", 122 | "enum": [ 123 | "I", 124 | "D", 125 | "-" 126 | ] 127 | } 128 | }, 129 | "additionalProperties": false 130 | }, 131 | "linea": { 132 | "type": "object", 133 | "patternProperties": { 134 | "^\\d{3,4}$": { 135 | "type": "string", 136 | "pattern": "^\\d+$" 137 | } 138 | }, 139 | "additionalProperties": false 140 | }, 141 | "tipos": { 142 | "type": "object", 143 | "patternProperties": { 144 | "^\\d{3,4}$": { 145 | "type": "string", 146 | "enum": [ 147 | "C", 148 | "D", 149 | "F", 150 | "M", 151 | "N", 152 | "R" 153 | ] 154 | } 155 | }, 156 | "additionalProperties": false 157 | } 158 | }, 159 | "required": [ 160 | "campos", 161 | "extras", 162 | "glosa", 163 | "tipos" 164 | ], 165 | "additionalProperties": true 166 | } 167 | -------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 8 | 170 9 | 2019-04-01 10 | 1 11 | 1 12 | 2 13 | 14 | 15 | 76354771-K 16 | INGENIERIA ENACON SPA 17 | Ingenieria y Construccion 18 | ENACONLTDA@GMAIL.COM 19 | 421000 20 | 078525666 21 | MERCED 753 16 ARBOLEDA DE QUIILOTA 22 | QUILLOTA 23 | QUILLOTA 24 | 25 | 26 | 96790240-3 27 | MINERA LOS PELAMBRES 28 | EXTRACCION Y PROCESAMIENTO DE COBRE 29 | Felipe Barria 30 | Av. Apoquindo 4001 1802 31 | LAS CONDES 32 | SANTIAGO 33 | 34 | 35 | 2517900 36 | 19.00 37 | 478401 38 | 2996301 39 | 40 | 41 | 42 | 1 43 | Tableros electricos 3 tom 44 | as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- 45 | 2.00 46 | Unid 47 | 1258950.00 48 | 2517900 49 | 50 | 51 | 1 52 | 801 53 | 4510083633 54 | 2019-03-22 55 | 56 |
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
57 | 2019-04-01T01:36:40 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ij2Qn6xOc2eRx3hwyO/GrzptoBk= 70 | 71 | 72 | 73 | fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk 74 | ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi 75 | wqSxDcYjTT6vXsLPrZk= 76 | 77 | 78 | 79 | 80 | 81 | pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl 82 | 9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN 83 | Uavs/9J+gR9BBMs/eYE= 84 | 85 | AQAB 86 | 87 | 88 | 89 | 90 | abc 91 | 92 | 93 | 94 |
-------------------------------------------------------------------------------- /src/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 8 | 170 9 | 2019-04-01 10 | 1 11 | 1 12 | 2 13 | 14 | 15 | 76354771-K 16 | INGENIERIA ENACON SPA 17 | Ingenieria y Construccion 18 | ENACONLTDA@GMAIL.COM 19 | 421000 20 | 078525666 21 | MERCED 753 16 ARBOLEDA DE QUIILOTA 22 | QUILLOTA 23 | QUILLOTA 24 | 25 | 26 | 96790240-3 27 | MINERA LOS PELAMBRES 28 | EXTRACCION Y PROCESAMIENTO DE COBRE 29 | Felipe Barria 30 | Av. Apoquindo 4001 1802 31 | LAS CONDES 32 | SANTIAGO 33 | 34 | 35 | 2517900 36 | 19.00 37 | 478401 38 | 2996301 39 | 40 | 41 | 42 | 1 43 | Tableros electricos 3 tom 44 | as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- 45 | 2.00 46 | Unid 47 | 1258950.00 48 | 2517900 49 | 50 | 51 | 1 52 | 801 53 | 4510083633 54 | 2019-03-22 55 | 56 |
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
57 | 2019-04-01T01:36:40 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ij2Qn6xOc2eRx3hwyO/GrzptoBk= 70 | 71 | 72 | 73 | fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk 74 | ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi 75 | wqSxDcYjTT6vXsLPrZk= 76 | 77 | 78 | 79 | 80 | 81 | pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl 82 | 9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN 83 | Uavs/9J+gR9BBMs/eYE= 84 | 85 | AQAB 86 | 87 | 88 | 89 | 90 | MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx 91 | +kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== 92 | 93 | 94 | 95 |
-------------------------------------------------------------------------------- /src/tests/test_libs_tz_utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import sys 4 | import unittest 5 | 6 | from cl_sii.libs.tz_utils import ( # noqa: F401 7 | _TZ_CL_SANTIAGO, 8 | TZ_UTC, 9 | PytzTimezone, 10 | convert_naive_dt_to_tz_aware, 11 | convert_tz_aware_dt_to_naive, 12 | dt_is_aware, 13 | dt_is_naive, 14 | get_now_tz_aware, 15 | validate_dt_tz, 16 | ) 17 | 18 | 19 | class FunctionsTest(unittest.TestCase): 20 | def test_get_now_tz_aware(self) -> None: 21 | # TODO: implement for 'get_now_tz_aware' 22 | # Reuse doctests/examples in function docstring. 23 | pass 24 | 25 | def test_convert_naive_dt_to_tz_aware(self) -> None: 26 | # TODO: implement for 'convert_naive_dt_to_tz_aware' 27 | # Reuse doctests/examples in function docstring. 28 | pass 29 | 30 | def test_convert_tz_aware_dt_to_naive(self) -> None: 31 | # TODO: implement for 'convert_tz_aware_dt_to_naive' 32 | # Reuse doctests/examples in function docstring. 33 | pass 34 | 35 | def test_dt_is_aware(self) -> None: 36 | # TODO: implement for 'dt_is_aware' 37 | # Reuse doctests/examples in function docstring. 38 | pass 39 | 40 | def test_dt_is_naive(self) -> None: 41 | # TODO: implement for 'dt_is_naive' 42 | # Reuse doctests/examples in function docstring. 43 | pass 44 | 45 | def test_validate_dt_tz(self) -> None: 46 | # TODO: implement for 'validate_dt_tz' 47 | pass 48 | 49 | def test_validate_dt_tz_tzinfo_zone_attribute_check(self) -> None: 50 | # Time zone: UTC. Source: Pytz: 51 | tzinfo_utc_pytz = TZ_UTC 52 | dt_with_tzinfo_utc_pytz = convert_naive_dt_to_tz_aware( 53 | datetime.datetime(2021, 1, 6, 15, 21), 54 | tzinfo_utc_pytz, 55 | ) 56 | 57 | # Time zone: UTC. Source: Python Standard Library: 58 | tzinfo_utc_stdlib = datetime.timezone.utc 59 | dt_with_tzinfo_utc_stdlib = datetime.datetime.fromisoformat('2021-01-06T15:04+00:00') 60 | 61 | # Time zone: Not UTC. Source: Pytz: 62 | tzinfo_not_utc_pytz = _TZ_CL_SANTIAGO 63 | dt_with_tzinfo_not_utc_pytz = convert_naive_dt_to_tz_aware( 64 | datetime.datetime(2021, 1, 6, 15, 21), 65 | tzinfo_not_utc_pytz, 66 | ) 67 | 68 | # Time zone: Not UTC. Source: Python Standard Library: 69 | tzinfo_not_utc_stdlib = datetime.timezone(datetime.timedelta(days=-1, seconds=75600)) 70 | dt_with_tzinfo_not_utc_stdlib = datetime.datetime.fromisoformat('2021-01-06T15:04-03:00') 71 | 72 | # Test datetimes with UTC time zone: 73 | expected_error_message = re.compile( 74 | r"^Object datetime.timezone.utc must have 'zone' attribute.$" 75 | ) 76 | with self.assertRaisesRegex(AssertionError, expected_error_message): 77 | validate_dt_tz(dt_with_tzinfo_utc_pytz, tzinfo_utc_stdlib) 78 | if sys.version_info >= (3, 11): 79 | expected_error_message = re.compile( 80 | r"^Object datetime.timezone.utc must have 'zone' attribute.$" 81 | ) 82 | else: 83 | expected_error_message = re.compile(r"^Object UTC must have 'zone' attribute.$") 84 | with self.assertRaisesRegex(AssertionError, expected_error_message): 85 | validate_dt_tz(dt_with_tzinfo_utc_stdlib, tzinfo_utc_pytz) 86 | 87 | # Test datetimes with non-UTC time zone: 88 | expected_error_message = re.compile( 89 | r"^Object" 90 | r" datetime.timezone\(datetime.timedelta\(days=-1, seconds=75600\)\)" 91 | r" must have 'zone' attribute.$" 92 | ) 93 | with self.assertRaisesRegex(AssertionError, expected_error_message): 94 | validate_dt_tz(dt_with_tzinfo_not_utc_pytz, tzinfo_not_utc_stdlib) # type: ignore 95 | if sys.version_info >= (3, 11): 96 | expected_error_message = re.compile( 97 | r"^Object" 98 | r" datetime.timezone\(datetime.timedelta\(days=-1, seconds=75600\)\)" 99 | r" must have 'zone' attribute.$" 100 | ) 101 | else: 102 | expected_error_message = re.compile( 103 | r"^Object" r" UTC-03:00" r" must have 'zone' attribute.$" 104 | ) 105 | with self.assertRaisesRegex(AssertionError, expected_error_message): 106 | validate_dt_tz(dt_with_tzinfo_not_utc_stdlib, tzinfo_not_utc_pytz) 107 | -------------------------------------------------------------------------------- /src/cl_sii/cte/data_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from datetime import date 5 | from decimal import Decimal 6 | from typing import Optional 7 | 8 | import pydantic 9 | 10 | 11 | @pydantic.dataclasses.dataclass( 12 | frozen=True, 13 | config=pydantic.ConfigDict( 14 | arbitrary_types_allowed=True, 15 | extra='forbid', 16 | ), 17 | ) 18 | class TaxpayerProvidedInfo: 19 | """ 20 | Información proporcionada por el contribuyente para fines tributarios (1) 21 | """ 22 | 23 | legal_representatives: Sequence[LegalRepresentative] 24 | company_formation: Sequence[LegalRepresentative] 25 | participation_in_existing_companies: Sequence[LegalRepresentative] 26 | 27 | 28 | @pydantic.dataclasses.dataclass( 29 | frozen=True, 30 | ) 31 | class LegalRepresentative: 32 | name: str 33 | """ 34 | Nombre o Razón social. 35 | """ 36 | rut: str 37 | """ 38 | RUT. 39 | """ 40 | incorporation_date: str 41 | """ 42 | Fecha de incorporación. 43 | """ 44 | 45 | 46 | @pydantic.dataclasses.dataclass( 47 | frozen=True, 48 | config=pydantic.ConfigDict( 49 | arbitrary_types_allowed=True, 50 | extra='forbid', 51 | ), 52 | ) 53 | class TaxpayerData: 54 | start_of_activities_date: Optional[date] 55 | """ 56 | Fecha de inicio de actividades. 57 | """ 58 | economic_activities: str 59 | """ 60 | Actividades Económicas 61 | """ 62 | tax_category: str 63 | """ 64 | Categoría Tributaria 65 | """ 66 | address: str 67 | """ 68 | Domicilio 69 | """ 70 | branches: Sequence[str] 71 | """ 72 | Sucursales 73 | """ 74 | last_filed_documents: Sequence[LastFiledDocument] 75 | """ 76 | Últimos documentos timbrados 77 | """ 78 | tax_observations: Optional[str] = None 79 | """ 80 | Observaciones tributarias 81 | """ 82 | 83 | 84 | @pydantic.dataclasses.dataclass( 85 | frozen=True, 86 | ) 87 | class LastFiledDocument: 88 | name: str 89 | date: date 90 | 91 | 92 | @pydantic.dataclasses.dataclass( 93 | frozen=True, 94 | config=pydantic.ConfigDict( 95 | arbitrary_types_allowed=True, 96 | extra='forbid', 97 | ), 98 | ) 99 | class TaxpayerProperties: 100 | """ 101 | Propiedades y Bienes Raíces (3) 102 | """ 103 | 104 | properties: Sequence[Property] 105 | 106 | 107 | @pydantic.dataclasses.dataclass( 108 | frozen=True, 109 | ) 110 | class Property: 111 | commune: Optional[str] 112 | """ 113 | Comuna 114 | """ 115 | role: Optional[str] 116 | """ 117 | Rol 118 | """ 119 | address: Optional[str] 120 | """ 121 | Dirección 122 | """ 123 | purpose: Optional[str] 124 | """ 125 | Destino 126 | """ 127 | fiscal_valuation: Optional[Decimal] 128 | """ 129 | Avalúo Fiscal 130 | """ 131 | overdue_installments: Optional[bool] 132 | """ 133 | Cuotas vencidas por pagar 134 | """ 135 | current_installments: Optional[bool] 136 | """ 137 | Cuotas vigentes por pagar 138 | """ 139 | condition: Optional[str] 140 | """ 141 | Condición 142 | """ 143 | 144 | ########################################################################### 145 | # Validators 146 | ########################################################################### 147 | 148 | @pydantic.field_validator('fiscal_valuation', mode='before') 149 | @classmethod 150 | def parse_fiscal_valuation(cls, v: Optional[str]) -> Optional[Decimal]: 151 | if isinstance(v, str): 152 | v = v.replace('.', '').replace(',', '.') 153 | return Decimal(v) 154 | return v 155 | 156 | @pydantic.field_validator('commune', 'role', 'address', 'purpose', 'condition') 157 | @classmethod 158 | def parse_str_fields(cls, v: Optional[str]) -> Optional[str]: 159 | if isinstance(v, str) and not v.strip(): 160 | return None 161 | return v 162 | 163 | @pydantic.field_validator('current_installments', 'overdue_installments', mode='before') 164 | @classmethod 165 | def parse_boolean_fields(cls, v: Optional[str | bool]) -> Optional[bool]: 166 | if isinstance(v, str): 167 | if v == 'NO': 168 | return False 169 | elif v == 'SI': 170 | return True 171 | else: 172 | return None 173 | if isinstance(v, bool): 174 | return v 175 | return None 176 | -------------------------------------------------------------------------------- /src/tests/test_cte_f29_data_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Sequence 4 | from unittest import TestCase 5 | 6 | from cl_sii.cte.f29 import data_models 7 | from cl_sii.rcv.data_models import PeriodoTributario 8 | from cl_sii.rut import Rut 9 | from . import cte_f29_factories 10 | 11 | 12 | class CteForm29Test(TestCase): 13 | """ 14 | Tests for ``cte.f29.data_models.CteForm29``. 15 | """ 16 | 17 | def test___str__(self) -> None: 18 | obj = cte_f29_factories.create_CteForm29( 19 | contribuyente_rut=Rut('1-9'), 20 | periodo_tributario=PeriodoTributario(year=2018, month=12), 21 | folio=1234567890, 22 | ) 23 | expected_output = ( 24 | "CteForm29(" 25 | "contribuyente_rut=Rut('1-9')," 26 | " periodo_tributario=PeriodoTributario(year=2018, month=12)," 27 | " folio=1234567890" 28 | ")" 29 | ) 30 | self.assertEqual(str(obj), expected_output) 31 | 32 | def test___repr__(self) -> None: 33 | obj = cte_f29_factories.create_CteForm29( 34 | contribuyente_rut=Rut('1-9'), 35 | periodo_tributario=PeriodoTributario(year=2018, month=12), 36 | folio=1234567890, 37 | ) 38 | expected_output = ( 39 | "CteForm29(" 40 | "contribuyente_rut=Rut('1-9')," 41 | " periodo_tributario=PeriodoTributario(year=2018, month=12)," 42 | " folio=1234567890" 43 | ")" 44 | ) 45 | self.assertEqual(repr(obj), expected_output) 46 | 47 | def test_code_field_mapping_class_attributes(self) -> None: 48 | obj = cte_f29_factories.create_CteForm29() 49 | for code, field_name in obj.CODE_FIELD_MAPPING.items(): 50 | if obj.get_field_name(code) is not None: 51 | self.assertTrue( 52 | hasattr(obj, field_name), 53 | msg=( 54 | f"Code '{code}' is associated to field '{field_name}'," 55 | f" but class '{obj.__class__.__name__}'" 56 | f" does not have that attribute." 57 | ), 58 | ) 59 | 60 | def test_code_field_mapping_value_uniqueness(self) -> None: 61 | obj = cte_f29_factories.create_CteForm29() 62 | 63 | code_field_names: Sequence[str] = [ 64 | field_name for field_name in obj.CODE_FIELD_MAPPING.values() if field_name is not None 65 | ] 66 | unique_code_field_names = set(code_field_names) 67 | 68 | self.assertEqual(len(code_field_names), len(unique_code_field_names)) 69 | 70 | def test_strict_codes(self) -> None: 71 | with self.assertRaises(KeyError): 72 | cte_f29_factories.create_CteForm29( 73 | _strict_codes=True, 74 | extra={ 75 | 999888777666: 'whatever', 76 | }, 77 | ) 78 | 79 | with self.assertLogs('cl_sii.cte.f29.data_models', level='WARNING') as assert_logs_cm: 80 | cte_f29_factories.create_CteForm29( 81 | _strict_codes=False, 82 | extra={ 83 | 123456: 42, 84 | 999888777666: 'whatever', 85 | }, 86 | ) 87 | self.assertEqual(len(assert_logs_cm.output), 1) 88 | self.assertIn( 89 | 'invalid or unknown SII Form 29 codes: 123456, 999888777666', 90 | assert_logs_cm.output[0], 91 | ) 92 | 93 | def test_get_field_name(self) -> None: 94 | # Test a valid code. 95 | self.assertEqual(data_models.CteForm29.get_field_name(7), 'folio') 96 | 97 | # Test invalid codes. 98 | with self.assertRaises(KeyError): 99 | data_models.CteForm29.get_field_name(999888777666, strict=True) 100 | 101 | self.assertIsNone(data_models.CteForm29.get_field_name(999888777666, strict=False)) 102 | 103 | # Test invalid type. 104 | with self.assertRaises(TypeError): 105 | data_models.CteForm29.get_field_name('999888777666') # type: ignore 106 | 107 | def test_natural_key(self) -> None: 108 | obj = cte_f29_factories.create_CteForm29( 109 | contribuyente_rut=Rut('1-9'), 110 | periodo_tributario=PeriodoTributario(year=2018, month=12), 111 | folio=1234567890, 112 | ) 113 | expected_output = data_models.CteForm29NaturalKey( 114 | contribuyente_rut=Rut('1-9'), 115 | periodo_tributario=PeriodoTributario(year=2018, month=12), 116 | folio=1234567890, 117 | ) 118 | self.assertEqual(obj.natural_key, expected_output) 119 | 120 | def test_get_all_codes(self) -> None: 121 | pass # TODO: Implement for 'get_all_codes'. 122 | 123 | def test_as_codes_dict(self) -> None: 124 | pass # TODO: Implement for 'as_codes_dict'. 125 | 126 | def test___getitem__(self) -> None: 127 | pass # TODO: Implement for '__getitem__'. 128 | 129 | def test___iter__(self) -> None: 130 | pass # TODO: Implement for '__iter__'. 131 | -------------------------------------------------------------------------------- /src/cl_sii/libs/rows_processing.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import logging 3 | from typing import Dict, Iterable, Optional, Sequence, Tuple 4 | 5 | import marshmallow 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class MaxRowsExceeded(RuntimeError): 12 | """ 13 | The maximum number of rows has been exceeded. 14 | """ 15 | 16 | 17 | ############################################################################### 18 | # iterators 19 | ############################################################################### 20 | 21 | 22 | def csv_rows_mm_deserialization_iterator( 23 | csv_reader: csv.DictReader, 24 | row_schema: marshmallow.Schema, 25 | n_rows_offset: int = 0, 26 | max_n_rows: Optional[int] = None, 27 | fields_to_remove_names: Optional[Sequence[str]] = None, 28 | ) -> Iterable[Tuple[int, Dict[str, object], Dict[str, object], dict]]: 29 | """ 30 | Marshmallow deserialization iterator over CSV rows. 31 | 32 | Iterate over ``csv_reader``, deserialize each row using ``row_schema`` 33 | and yield the data before and after deserialization, plus any 34 | validation/deserialization errors. 35 | 36 | .. note:: The CSV header row is omitted, obviously. 37 | 38 | :param csv_reader: 39 | :param row_schema: 40 | Marshmallow schema for deserializing each CSV row 41 | :param n_rows_offset: 42 | (optional) number of rows to skip (and not deserialize) 43 | :param max_n_rows: 44 | (optional) max number of rows to deserialize (raise exception 45 | if exceeded); ``None`` means no limit 46 | :param fields_to_remove_names: 47 | (optional) the name of each field that must be removed (if it exists) 48 | from the row 49 | :returns: 50 | yields a tuple of (``row_ix`` (1-based), ``row_data``, 51 | ``deserialized_row_data``, ``validation_errors``) 52 | :raises MaxRowsExceeded: 53 | number of data rows processed exceeded ``max_n_rows`` 54 | :raises RuntimeError: 55 | on CSV error when iterating over ``csv_reader`` 56 | 57 | """ 58 | rows_iterator: Iterable[Dict[str, object]] = csv_reader 59 | iterator = rows_mm_deserialization_iterator( 60 | rows_iterator, row_schema, n_rows_offset, max_n_rows, fields_to_remove_names 61 | ) 62 | 63 | try: 64 | # note: we chose not to use 'yield from' to be explicit about what we are yielding. 65 | for row_ix, row_data, deserialized_row_data, validation_errors in iterator: 66 | yield row_ix, row_data, deserialized_row_data, validation_errors 67 | except csv.Error as exc: 68 | exc_msg = f"CSV error for line {csv_reader.line_num} of CSV file." 69 | raise RuntimeError(exc_msg) from exc 70 | 71 | 72 | def rows_mm_deserialization_iterator( 73 | rows_iterator: Iterable[Dict[str, object]], 74 | row_schema: marshmallow.Schema, 75 | n_rows_offset: int = 0, 76 | max_n_rows: Optional[int] = None, 77 | fields_to_remove_names: Optional[Sequence[str]] = None, 78 | ) -> Iterable[Tuple[int, Dict[str, object], Dict[str, object], dict]]: 79 | """ 80 | Marshmallow deserialization iterator. 81 | 82 | Iterate over ``rows_iterator``, deserialize each row using ``row_schema`` 83 | and yield the data before and after deserialization, plus any 84 | validation/deserialization errors. 85 | 86 | :param rows_iterator: 87 | :param row_schema: 88 | Marshmallow schema for deserializing each row 89 | :param n_rows_offset: 90 | (optional) number of rows to skip (and not deserialize) 91 | :param max_n_rows: 92 | (optional) max number of rows to deserialize (raise exception 93 | if exceeded); ``None`` means no limit 94 | :param fields_to_remove_names: 95 | (optional) the name of each field that must be removed (if it exists) 96 | from the row 97 | :returns: 98 | yields a tuple of (``row_ix`` (1-based), ``row_data``, 99 | ``deserialized_row_data``, ``validation_errors``) 100 | :raises MaxRowsExceeded: 101 | number of data rows processed exceeded ``max_n_rows`` 102 | 103 | """ 104 | if not n_rows_offset >= 0: 105 | raise ValueError("Param 'n_rows_offset' must be an integer >= 0.") 106 | 107 | fields_to_remove_names = fields_to_remove_names or () 108 | 109 | for row_ix, row_data in enumerate(rows_iterator, start=1): 110 | if max_n_rows is not None and row_ix > max_n_rows + n_rows_offset: 111 | raise MaxRowsExceeded(f"Exceeded 'max_n_rows' limit: {max_n_rows}.") 112 | 113 | if row_ix <= n_rows_offset: 114 | continue 115 | 116 | for _field_name in fields_to_remove_names: 117 | row_data.pop(_field_name, None) 118 | 119 | try: 120 | deserialized_row_data: dict = row_schema.load(row_data) 121 | raised_validation_errors: dict = {} 122 | except marshmallow.ValidationError as exc: 123 | deserialized_row_data = {} 124 | raised_validation_errors = dict(exc.normalized_messages()) 125 | 126 | validation_errors = raised_validation_errors 127 | 128 | yield row_ix, row_data, deserialized_row_data, validation_errors 129 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for Continuous Integration 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_call: 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | PYTHON_VIRTUALENV_ACTIVATE: venv/bin/activate 13 | 14 | jobs: 15 | pre-build: 16 | name: Pre-Build 17 | runs-on: ubuntu-22.04 18 | 19 | steps: 20 | - run: "true" 21 | 22 | build: 23 | name: Build 24 | needs: 25 | - pre-build 26 | runs-on: ubuntu-22.04 27 | 28 | strategy: 29 | matrix: 30 | python_version: 31 | - "3.9" 32 | - "3.10" 33 | - "3.11" 34 | - "3.12" 35 | - "3.13" 36 | 37 | steps: 38 | - name: Check Out VCS Repository 39 | uses: actions/checkout@v6.0.0 40 | 41 | - name: Set Up Python ${{ matrix.python_version }} 42 | id: set_up_python 43 | uses: actions/setup-python@v6.1.0 44 | with: 45 | python-version: "${{ matrix.python_version }}" 46 | check-latest: true 47 | 48 | - name: Install Pip 49 | run: make python-pip-install 50 | 51 | - name: Create Python Virtual Environment 52 | run: make python-virtualenv PYTHON_VIRTUALENV_DIR="venv" 53 | 54 | - name: Restoring/Saving Cache 55 | uses: actions/cache@v4.3.0 56 | with: 57 | path: | 58 | .tox 59 | venv 60 | key: py-v1-deps-${{ runner.os }}-${{ steps.set_up_python.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'requirements.txt', 'requirements-dev.txt', 'Makefile', 'make/**.mk') }} 61 | 62 | - name: Install Dependencies 63 | run: | 64 | source "$PYTHON_VIRTUALENV_ACTIVATE" 65 | make install-deps-dev 66 | 67 | - name: Install Library 68 | run: | 69 | source "$PYTHON_VIRTUALENV_ACTIVATE" 70 | make install-dev 71 | 72 | test: 73 | name: Test 74 | needs: 75 | - test- 76 | runs-on: ubuntu-22.04 77 | 78 | steps: 79 | - name: Do Nothing 80 | run: | 81 | echo "This job is only to have a common 'needs' for future jobs after 'test'." 82 | 83 | test-: 84 | name: Test 85 | needs: 86 | - build 87 | runs-on: ubuntu-22.04 88 | 89 | strategy: 90 | matrix: 91 | python_version: 92 | - "3.9" 93 | - "3.10" 94 | - "3.11" 95 | - "3.12" 96 | - "3.13" 97 | 98 | steps: 99 | - name: Check Out VCS Repository 100 | uses: actions/checkout@v6.0.0 101 | 102 | - name: Set Up Python ${{ matrix.python_version }} 103 | id: set_up_python 104 | uses: actions/setup-python@v6.1.0 105 | with: 106 | python-version: "${{ matrix.python_version }}" 107 | check-latest: true 108 | 109 | - name: Restoring/Saving Cache 110 | uses: actions/cache@v4.3.0 111 | with: 112 | path: | 113 | .tox 114 | venv 115 | key: py-v1-deps-${{ runner.os }}-${{ steps.set_up_python.outputs.python-version }}-${{ hashFiles('pyproject.toml', 'requirements.txt', 'requirements-dev.txt', 'Makefile', 'make/**.mk') }} 116 | fail-on-cache-miss: true 117 | 118 | - name: Set Tox Environment 119 | id: set_tox_environment 120 | run: | 121 | # Set Tox environment to the installed Python version. 122 | tox_env=$( 123 | python -c 'import sys; v = sys.version_info; print("py{}{}".format(v.major, v.minor))' 124 | ) 125 | 126 | echo "tox_env=${tox_env:?}" >> "$GITHUB_OUTPUT" 127 | 128 | - name: Test 129 | run: | 130 | source "$PYTHON_VIRTUALENV_ACTIVATE" 131 | make test 132 | env: 133 | TOXENV: ${{ steps.set_tox_environment.outputs.tox_env }} 134 | 135 | - name: Lint 136 | run: | 137 | source "$PYTHON_VIRTUALENV_ACTIVATE" 138 | make lint 139 | 140 | - name: Test Coverage 141 | run: | 142 | source "$PYTHON_VIRTUALENV_ACTIVATE" 143 | make test-coverage 144 | 145 | - name: Test Coverage Report 146 | run: | 147 | source "$PYTHON_VIRTUALENV_ACTIVATE" 148 | make test-coverage-report 149 | 150 | - name: Upload coverage reports to Codecov 151 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 152 | with: 153 | token: ${{ secrets.CODECOV_TOKEN }} 154 | directory: ./test-reports/coverage/ 155 | fail_ci_if_error: true 156 | 157 | - name: Check that compiled Python dependency manifests are up-to-date with their sources 158 | # FIXME: There are issues related to testing with multiple Python versions. 159 | if: ${{ startsWith(steps.set_up_python.outputs.python-version, '3.9.') }} 160 | run: | 161 | source "$PYTHON_VIRTUALENV_ACTIVATE" 162 | make python-deps-sync-check 163 | 164 | - name: Store Artifacts 165 | if: ${{ always() }} 166 | uses: actions/upload-artifact@v5.0.0 167 | with: 168 | name: test_reports_${{ matrix.python_version }} 169 | path: test-reports/ 170 | if-no-files-found: warn 171 | --------------------------------------------------------------------------------