├── 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 | [](https://pypi.org/project/cl-sii/)
4 | [](https://pypi.org/project/cl-sii/)
5 | [](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) | [](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) | [](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 | | [](https://codecov.io/gh/cordada/lib-cl-sii-python) | [](https://codeclimate.com/github/fyntex/lib-cl-sii-python/maintainability) | [](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-K| 33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+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-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+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-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+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-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+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-K33 | 1702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA33 | 1701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40DKFS7bNYRpVYLNEII+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 |
--------------------------------------------------------------------------------